tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See CLI examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from dateutil.tz import tzlocal 41from time import sleep 42 43import re 44import json 45import requests 46import traceback as tb 47from typing import Union 48 49from multiprocessing import cpu_count, Lock 50from multiprocessing.pool import ThreadPool 51import pandas as pd 52 53from mako.template import Template # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 54from Templates import * # Some html-templates used by reporting methods in TKSBrokerAPI module 55from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 56from TradeRoutines import * # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module 57 58from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator) 59from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 60 61import UniLogger as uLog # Logger for TKSBrokerAPI 62 63 64# --- Common technical parameters: 65 66PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 67uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 68uLogger.level = 10 # debug level by default for TKSBrokerAPI module 69uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 70 71__version__ = "1.6" # The "major.minor" version setup here, but build number define at the build-server only 72 73CPU_COUNT = cpu_count() # host's real CPU count 74CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 75 76 77class TinkoffBrokerServer: 78 """ 79 This class implements methods to work with Tinkoff broker server. 80 81 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 82 83 About `token`: https://tinkoff.github.io/investAPI/token/ 84 """ 85 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 86 """ 87 Main class init. 88 89 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 90 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 91 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 92 :param useCache: use default cache file with raw data to use instead of `iList`. 93 True by default. Cache is auto-update if new day has come. 94 If you don't want to use cache and always updates raw data then set `useCache=False`. 95 :param defaultCache: path to default cache file. `dump.json` by default. 96 """ 97 if token is None or not token: 98 try: 99 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 100 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 101 102 except KeyError: 103 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 104 raise Exception("Token required") 105 106 else: 107 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 108 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 109 110 if accountId is None or not accountId: 111 try: 112 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 113 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 114 115 except KeyError: 116 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 117 118 else: 119 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 120 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 121 122 self.version = __version__ # duplicate here used TKSBrokerAPI main version 123 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 124 125 Latest version: https://pypi.org/project/tksbrokerapi/ 126 """ 127 128 self.__lock = Lock() # initialize multiprocessing mutex lock 129 130 self.aliases = TKS_TICKER_ALIASES 131 """Some aliases instead official tickers. 132 133 See also: `TKSEnums.TKS_TICKER_ALIASES` 134 """ 135 136 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 137 138 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 139 140 self._ticker = "" 141 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 142 143 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 144 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 145 146 See also: `SearchByTicker()`, `SearchInstruments()`. 147 """ 148 149 self._figi = "" 150 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 151 152 See also: `SearchByFIGI()`, `SearchInstruments()`. 153 """ 154 155 self.depth = 1 156 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 157 158 See also: `GetCurrentPrices()`. 159 """ 160 161 self.server = r"https://invest-public-api.tinkoff.ru/rest" 162 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 163 164 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 165 """ 166 167 uLogger.debug("Broker API server: {}".format(self.server)) 168 169 self.timeout = 15 170 """Server operations timeout in seconds. Default: `15`. 171 172 See also: `SendAPIRequest()`. 173 """ 174 175 self.headers = { 176 "Content-Type": "application/json", 177 "accept": "application/json", 178 "Authorization": "Bearer {}".format(self.token), 179 "x-app-name": "Tim55667757.TKSBrokerAPI", 180 } 181 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 182 183 See also: `SendAPIRequest()`. 184 """ 185 186 self.body = None 187 """Request body which send to broker server. Default: `None`. 188 189 See also: `SendAPIRequest()`. 190 """ 191 192 self.moreDebug = False 193 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 194 195 self.useHTMLReports = False 196 """ 197 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 198 199 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 200 """ 201 202 self.historyFile = None 203 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 204 205 See also: `History()`. 206 """ 207 208 self.htmlHistoryFile = "index.html" 209 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 210 211 See also: `ShowHistoryChart()`. 212 """ 213 214 self.instrumentsFile = "instruments.md" 215 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 216 217 See also: `ShowInstrumentsInfo()`. 218 """ 219 220 self.searchResultsFile = "search-results.md" 221 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 222 223 See also: `SearchInstruments()`. 224 """ 225 226 self.pricesFile = "prices.md" 227 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 228 229 See also: `GetListOfPrices()`. 230 """ 231 232 self.infoFile = "info.md" 233 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 234 235 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 236 """ 237 238 self.bondsXLSXFile = "ext-bonds.xlsx" 239 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 240 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 241 242 See also: `ExtendBondsData()`. 243 """ 244 245 self.calendarFile = "calendar.md" 246 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 247 248 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 249 250 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 251 """ 252 253 self.overviewFile = "overview.md" 254 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 255 256 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 257 """ 258 259 self.overviewDigestFile = "overview-digest.md" 260 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 261 262 See also: `Overview()` with parameter `details="digest"`. 263 """ 264 265 self.overviewPositionsFile = "overview-positions.md" 266 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 267 268 See also: `Overview()` with parameter `details="positions"`. 269 """ 270 271 self.overviewOrdersFile = "overview-orders.md" 272 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 273 274 See also: `Overview()` with parameter `details="orders"`. 275 """ 276 277 self.overviewAnalyticsFile = "overview-analytics.md" 278 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 279 280 See also: `Overview()` with parameter `details="analytics"`. 281 """ 282 283 self.overviewBondsCalendarFile = "overview-calendar.md" 284 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 285 286 See also: `Overview()` with parameter `details="calendar"`. 287 """ 288 289 self.reportFile = "deals.md" 290 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 291 292 See also: `Deals()`. 293 """ 294 295 self.withdrawalLimitsFile = "limits.md" 296 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 297 298 See also: `OverviewLimits()` and `RequestLimits()`. 299 """ 300 301 self.userInfoFile = "user-info.md" 302 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 303 304 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 305 """ 306 307 self.userAccountsFile = "accounts.md" 308 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 309 310 See also: `OverviewAccounts()`, `RequestAccounts()`. 311 """ 312 313 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 314 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 315 316 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 317 318 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 319 """ 320 321 self.iList = None # init iList for raw instruments data 322 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 323 324 See also: `Listing()`, `DumpInstruments()`. 325 """ 326 327 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 328 if useCache: 329 if os.path.exists(self.iListDumpFile): 330 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 331 curTime = datetime.now(tzutc()) 332 333 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 334 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 335 336 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 337 338 else: 339 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 340 341 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 342 os.path.abspath(self.iListDumpFile), 343 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 344 )) 345 346 else: 347 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 348 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 349 350 else: 351 self.iList = self.Listing() # request new raw instruments data from broker server 352 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 353 354 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 355 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 356 357 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 358 """ 359 360 @property 361 def ticker(self) -> str: 362 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 363 364 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 365 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 366 367 See also: `SearchByTicker()`, `SearchInstruments()`. 368 """ 369 return self._ticker 370 371 @ticker.setter 372 def ticker(self, value): 373 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 374 375 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 376 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 377 378 See also: `SearchByTicker()`, `SearchInstruments()`. 379 """ 380 self._ticker = str(value).upper() # Tickers may be upper case only 381 382 @property 383 def figi(self) -> str: 384 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 385 386 See also: `SearchByFIGI()`, `SearchInstruments()`. 387 """ 388 return self._figi 389 390 @figi.setter 391 def figi(self, value): 392 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 393 394 See also: `SearchByFIGI()`, `SearchInstruments()`. 395 """ 396 self._figi = str(value).upper() # FIGI may be upper case only 397 398 def _ParseJSON(self, rawData="{}") -> dict: 399 """ 400 Parse JSON from response string. 401 402 :param rawData: this is a string with JSON-formatted text. 403 :return: JSON (dictionary), parsed from server response string. 404 """ 405 responseJSON = json.loads(rawData) if rawData else {} 406 407 if self.moreDebug: 408 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 409 410 return responseJSON 411 412 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 413 """ 414 Send GET or POST request to broker server and receive JSON object. 415 416 self.header: must be defining with dictionary of headers. 417 self.body: if define then used as request body. None by default. 418 self.timeout: global request timeout, 15 seconds by default. 419 :param url: url with REST request. 420 :param reqType: send "GET" or "POST" request. "GET" by default. 421 :param retry: how many times retry after first request if an 5xx server errors occurred. 422 :param pause: sleep time in seconds between retries. 423 :return: response JSON (dictionary) from broker. 424 """ 425 if reqType.upper() not in ("GET", "POST"): 426 uLogger.error("You can define request type: `GET` or `POST`!") 427 raise Exception("Incorrect value") 428 429 if self.moreDebug: 430 uLogger.debug("Request parameters:") 431 uLogger.debug(" - REST API URL: {}".format(url)) 432 uLogger.debug(" - request type: {}".format(reqType)) 433 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 434 uLogger.debug(" - body:\n{}".format(self.body)) 435 436 # fast hack to avoid all operations with some tickers/FIGI 437 responseJSON = {} 438 oK = True 439 for item in self.exclude: 440 if item in url: 441 if self.moreDebug: 442 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 443 444 oK = False 445 break 446 447 if oK: 448 with self.__lock: # acquire the mutex lock 449 counter = 0 450 response = None 451 errMsg = "" 452 453 while not response and counter <= retry: 454 if reqType == "GET": 455 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 456 457 if reqType == "POST": 458 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 459 460 if self.moreDebug: 461 uLogger.debug("Response:") 462 uLogger.debug(" - status code: {}".format(response.status_code)) 463 uLogger.debug(" - reason: {}".format(response.reason)) 464 uLogger.debug(" - body length: {}".format(len(response.text))) 465 uLogger.debug(" - headers:\n{}".format(response.headers)) 466 467 # Server returns some headers: 468 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 469 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 470 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 471 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 472 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 473 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 474 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 475 sleep(rateLimitWait) 476 477 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 478 if 400 <= response.status_code < 500: 479 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 480 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 481 482 if "code" in response.text and "message" in response.text: 483 msgDict = self._ParseJSON(rawData=response.text) 484 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 485 486 counter = retry + 1 # do not retry for 4xx errors 487 488 if 500 <= response.status_code < 600: 489 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 490 uLogger.debug(" - not oK, {}".format(errMsg)) 491 492 if "code" in response.text and "message" in response.text: 493 errMsgDict = self._ParseJSON(rawData=response.text) 494 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 495 496 counter += 1 497 498 if counter <= retry: 499 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 500 sleep(pause) 501 502 responseJSON = self._ParseJSON(rawData=response.text) 503 504 if errMsg: 505 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 506 uLogger.error(" - not oK, {}".format(errMsg)) 507 508 return responseJSON 509 510 def _IUpdater(self, iType: str) -> tuple: 511 """ 512 Request instrument by type from server. See available API methods for instruments: 513 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 514 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 515 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 516 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 517 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 518 519 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 520 :return: tuple with iType name and list of available instruments of current type for defined user token. 521 """ 522 result = [] 523 524 if iType in TKS_INSTRUMENTS: 525 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 526 527 # all instruments have the same body in API v2 requests: 528 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 529 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 530 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 531 532 return iType, result 533 534 def _IWrapper(self, kwargs): 535 """ 536 Wrapper runs instrument's update method `_IUpdater()`. 537 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 538 """ 539 return self._IUpdater(**kwargs) 540 541 def Listing(self) -> dict: 542 """ 543 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 544 545 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 546 """ 547 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 548 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 549 550 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 551 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 552 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 553 554 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 555 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 556 poolUpdater.close() # close the thread pool 557 poolUpdater.join() # wait a moment until all data returns from threads 558 559 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 560 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 561 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 562 563 # calculate minimum price increment (step) for all instruments and set up instrument's type: 564 for iType in iList.keys(): 565 for ticker in iList[iType]: 566 iList[iType][ticker]["type"] = iType 567 568 if "minPriceIncrement" in iList[iType][ticker].keys(): 569 iList[iType][ticker]["step"] = NanoToFloat( 570 iList[iType][ticker]["minPriceIncrement"]["units"], 571 iList[iType][ticker]["minPriceIncrement"]["nano"], 572 ) 573 574 else: 575 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 576 577 return iList 578 579 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 580 """ 581 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 582 583 See also: `DumpInstruments()`, `Listing()`. 584 585 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 586 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 587 """ 588 if self.iListDumpFile is None or not self.iListDumpFile: 589 uLogger.error("Output name of dump file must be defined!") 590 raise Exception("Filename required") 591 592 if not self.iList or forceUpdate: 593 self.iList = self.Listing() 594 595 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 596 597 # Save as XLSX with separated sheets for every type of instruments: 598 with pd.ExcelWriter( 599 path=xlsxDumpFile, 600 date_format=TKS_DATE_FORMAT, 601 datetime_format=TKS_DATE_TIME_FORMAT, 602 mode="w", 603 ) as writer: 604 for iType in TKS_INSTRUMENTS: 605 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 606 df = df[sorted(df)] # sorted by column names 607 df = df.applymap( 608 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 609 na_action="ignore", 610 ) # converting numbers from nano-type to float in every cell 611 df.to_excel( 612 writer, 613 sheet_name=iType, 614 encoding="UTF-8", 615 freeze_panes=(1, 1), 616 ) # saving as XLSX-file with freeze first row and column as headers 617 618 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 619 620 def DumpInstruments(self, forceUpdate: bool = True) -> str: 621 """ 622 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 623 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 624 625 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 626 627 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 628 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 629 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 630 """ 631 if self.iListDumpFile is None or not self.iListDumpFile: 632 uLogger.error("Output name of dump file must be defined!") 633 raise Exception("Filename required") 634 635 if not self.iList or forceUpdate: 636 self.iList = self.Listing() 637 638 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 639 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 640 fH.write(jsonDump) 641 642 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 643 644 return jsonDump 645 646 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 647 """ 648 Show information about one instrument defined by json data and prints it in Markdown format. 649 650 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 651 652 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 653 :param show: if `True` then also printing information about instrument and its current price. 654 :return: multilines text in Markdown format with information about one instrument. 655 """ 656 splitLine = "| | |\n" 657 infoText = "" 658 659 if iJSON is not None and iJSON and isinstance(iJSON, dict): 660 info = [ 661 "# Main information\n\n", 662 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 663 "| Parameters | Values |\n", 664 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 665 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 666 "| Full name: | {:<54} |\n".format(iJSON["name"]), 667 ] 668 669 if "sector" in iJSON.keys() and iJSON["sector"]: 670 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 671 672 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 673 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 674 675 info.extend([ 676 splitLine, 677 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 678 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 679 ]) 680 681 if "isin" in iJSON.keys() and iJSON["isin"]: 682 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 683 684 if "classCode" in iJSON.keys(): 685 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 686 687 info.extend([ 688 splitLine, 689 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 690 splitLine, 691 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 692 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 693 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 694 ]) 695 696 if iJSON["figi"]: 697 self._figi = iJSON["figi"] 698 iJSON = iJSON | self.RequestTradingStatus() 699 700 info.extend([ 701 splitLine, 702 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 703 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 704 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 705 ]) 706 707 info.append(splitLine) 708 709 if "type" in iJSON.keys() and iJSON["type"]: 710 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 711 712 if "shareType" in iJSON.keys() and iJSON["shareType"]: 713 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 714 715 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 716 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 717 718 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 719 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 720 721 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 722 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 723 724 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 725 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 726 727 if "focusType" in iJSON.keys() and iJSON["focusType"]: 728 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 729 730 if "assetType" in iJSON.keys() and iJSON["assetType"]: 731 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 732 733 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 734 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 735 736 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 737 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 738 739 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 740 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 741 742 if "currency" in iJSON.keys(): 743 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 744 745 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 746 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 747 748 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 749 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 750 751 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 752 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 753 754 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 755 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 756 757 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 758 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 759 760 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 761 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 762 763 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 764 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 765 766 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 767 info.append("| Perpetual bond: | Yes |\n") 768 769 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 770 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 771 772 iExt = None 773 if iJSON["type"] == "Bonds": 774 info.extend([ 775 splitLine, 776 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 777 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 778 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 779 iJSON["nominal"]["currency"], 780 )), 781 ]) 782 783 if "floatingCouponFlag" in iJSON.keys(): 784 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 785 786 if "amortizationFlag" in iJSON.keys(): 787 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 788 789 info.append(splitLine) 790 791 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 792 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 793 794 if iJSON["figi"]: 795 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 796 797 info.extend([ 798 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 799 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 800 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 801 ]) 802 803 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 804 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 805 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 806 iJSON["aciValue"]["currency"] 807 ))) 808 809 if "currentPrice" in iJSON.keys(): 810 info.append(splitLine) 811 812 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 813 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 814 815 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 816 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 817 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 818 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 819 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 820 821 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 822 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 823 824 info.extend([ 825 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 826 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 827 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 828 )), 829 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 830 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 831 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 832 )), 833 "| Changes between last deal price and last close | {:<54} |\n".format( 834 "{:.2f}%{}".format( 835 iJSON["currentPrice"]["changes"], 836 " ({}{:.2f} {})".format( 837 "+" if bondChangesDelta > 0 else "", 838 bondChangesDelta, 839 aciCurrency 840 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 841 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 842 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 843 currency 844 ), 845 ) 846 ), 847 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 848 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 849 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 850 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 851 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 852 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 853 )), 854 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 855 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 856 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 857 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 858 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 859 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 860 )), 861 ]) 862 863 if "lot" in iJSON.keys(): 864 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 865 866 if "step" in iJSON.keys() and iJSON["step"] != 0: 867 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 868 869 # Add bond payment calendar: 870 if iJSON["type"] == "Bonds": 871 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 872 info.extend(["\n#", strCalendar]) 873 874 infoText += "".join(info) 875 876 if show: 877 uLogger.info("{}".format(infoText)) 878 879 else: 880 uLogger.debug("{}".format(infoText)) 881 882 if self.infoFile is not None: 883 with open(self.infoFile, "w", encoding="UTF-8") as fH: 884 fH.write(infoText) 885 886 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 887 888 if self.useHTMLReports: 889 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 890 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 891 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 892 893 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 894 895 return infoText 896 897 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 898 """ 899 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 900 901 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 902 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 903 :return: JSON formatted data with information about instrument. 904 """ 905 tickerJSON = {} 906 if self.moreDebug: 907 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 908 909 if not self._ticker: 910 uLogger.warning("self._ticker variable is not be empty!") 911 912 else: 913 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 914 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 915 raise Exception("Instrument not allowed") 916 917 if not self.iList: 918 self.iList = self.Listing() 919 920 if self._ticker in self.iList["Shares"].keys(): 921 tickerJSON = self.iList["Shares"][self._ticker] 922 if self.moreDebug: 923 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 924 925 elif self._ticker in self.iList["Currencies"].keys(): 926 tickerJSON = self.iList["Currencies"][self._ticker] 927 if self.moreDebug: 928 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 929 930 elif self._ticker in self.iList["Bonds"].keys(): 931 tickerJSON = self.iList["Bonds"][self._ticker] 932 if self.moreDebug: 933 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 934 935 elif self._ticker in self.iList["Etfs"].keys(): 936 tickerJSON = self.iList["Etfs"][self._ticker] 937 if self.moreDebug: 938 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 939 940 elif self._ticker in self.iList["Futures"].keys(): 941 tickerJSON = self.iList["Futures"][self._ticker] 942 if self.moreDebug: 943 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 944 945 if tickerJSON: 946 self._figi = tickerJSON["figi"] 947 948 if requestPrice: 949 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 950 951 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 952 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 953 954 else: 955 tickerJSON["currentPrice"]["changes"] = 0 956 957 if show: 958 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 959 960 else: 961 if show: 962 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 963 964 return tickerJSON 965 966 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 967 """ 968 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 969 970 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 971 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 972 :return: JSON formatted data with information about instrument. 973 """ 974 figiJSON = {} 975 if self.moreDebug: 976 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 977 978 if not self._figi: 979 uLogger.warning("self._figi variable is not be empty!") 980 981 else: 982 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 983 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 984 raise Exception("Instrument not allowed") 985 986 if not self.iList: 987 self.iList = self.Listing() 988 989 for item in self.iList["Shares"].keys(): 990 if self._figi == self.iList["Shares"][item]["figi"]: 991 figiJSON = self.iList["Shares"][item] 992 993 if self.moreDebug: 994 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 995 996 break 997 998 if not figiJSON: 999 for item in self.iList["Currencies"].keys(): 1000 if self._figi == self.iList["Currencies"][item]["figi"]: 1001 figiJSON = self.iList["Currencies"][item] 1002 1003 if self.moreDebug: 1004 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1005 1006 break 1007 1008 if not figiJSON: 1009 for item in self.iList["Bonds"].keys(): 1010 if self._figi == self.iList["Bonds"][item]["figi"]: 1011 figiJSON = self.iList["Bonds"][item] 1012 1013 if self.moreDebug: 1014 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1015 1016 break 1017 1018 if not figiJSON: 1019 for item in self.iList["Etfs"].keys(): 1020 if self._figi == self.iList["Etfs"][item]["figi"]: 1021 figiJSON = self.iList["Etfs"][item] 1022 1023 if self.moreDebug: 1024 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1025 1026 break 1027 1028 if not figiJSON: 1029 for item in self.iList["Futures"].keys(): 1030 if self._figi == self.iList["Futures"][item]["figi"]: 1031 figiJSON = self.iList["Futures"][item] 1032 1033 if self.moreDebug: 1034 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1035 1036 break 1037 1038 if figiJSON: 1039 self._figi = figiJSON["figi"] 1040 self._ticker = figiJSON["ticker"] 1041 1042 if requestPrice: 1043 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1044 1045 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1046 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1047 1048 else: 1049 figiJSON["currentPrice"]["changes"] = 0 1050 1051 if show: 1052 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1053 1054 else: 1055 if show: 1056 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1057 1058 return figiJSON 1059 1060 def GetCurrentPrices(self, show: bool = True) -> dict: 1061 """ 1062 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1063 `{"buy": [{"price": 1243.8, "quantity": 193}, 1064 {"price": 1244.0, "quantity": 168}, 1065 {"price": 1244.8, "quantity": 5}, 1066 {"price": 1245.0, "quantity": 61}, 1067 {"price": 1245.4, "quantity": 60}], 1068 "sell": [{"price": 1243.6, "quantity": 8}, 1069 {"price": 1242.6, "quantity": 10}, 1070 {"price": 1242.4, "quantity": 18}, 1071 {"price": 1242.2, "quantity": 50}, 1072 {"price": 1242.0, "quantity": 113}], 1073 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1074 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1075 - sell: list of dicts with Buyers prices, 1076 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1077 - quantity: volume value by current price in lots, 1078 - limitUp: current trade session limit price, maximum, 1079 - limitDown: current trade session limit price, minimum, 1080 - lastPrice: last deal price of the instrument, 1081 - closePrice: previous trade session close price of the instrument. 1082 1083 See also: `SearchByTicker()` and `SearchByFIGI()`. 1084 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1085 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1086 1087 :param show: if `True` then print DOM to log and console. 1088 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1089 If an error occurred then returns an empty record: 1090 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1091 """ 1092 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1093 1094 if self.depth < 1: 1095 uLogger.error("Depth of Market (DOM) must be >=1!") 1096 raise Exception("Incorrect value") 1097 1098 if not (self._ticker or self._figi): 1099 uLogger.error("self._ticker or self._figi variables must be defined!") 1100 raise Exception("Ticker or FIGI required") 1101 1102 if self._ticker and not self._figi: 1103 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1104 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1105 1106 if not self._ticker and self._figi: 1107 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1108 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1109 1110 if not self._figi: 1111 uLogger.error("FIGI is not defined!") 1112 raise Exception("Ticker or FIGI required") 1113 1114 else: 1115 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1116 1117 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1118 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1119 self.body = str({"figi": self._figi, "depth": self.depth}) 1120 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1121 1122 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1123 # list of dicts with sellers orders: 1124 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1125 1126 # list of dicts with buyers orders: 1127 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1128 1129 # max price of instrument at this time: 1130 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1131 1132 # min price of instrument at this time: 1133 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1134 1135 # last price of deal with instrument: 1136 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1137 1138 # last close price of instrument: 1139 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1140 1141 else: 1142 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1143 uLogger.debug("Server response: {}".format(pricesResponse)) 1144 1145 if show: 1146 if prices["buy"] or prices["sell"]: 1147 info = [ 1148 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1149 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1150 self._ticker, 1151 self._figi, 1152 self.depth, 1153 ), 1154 "-" * 60, "\n", 1155 " Orders of Buyers | Orders of Sellers\n", 1156 "-" * 60, "\n", 1157 " Sell prices (volumes) | Buy prices (volumes)\n", 1158 "-" * 60, "\n", 1159 ] 1160 1161 if not prices["buy"]: 1162 info.append(" | No orders!\n") 1163 sumBuy = 0 1164 1165 else: 1166 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1167 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1168 for item in maxMinSorted: 1169 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1170 1171 if not prices["sell"]: 1172 info.append("No orders! |\n") 1173 sumSell = 0 1174 1175 else: 1176 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1177 for item in prices["sell"]: 1178 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1179 1180 info.extend([ 1181 "-" * 60, "\n", 1182 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1183 "-" * 60, "\n", 1184 ]) 1185 1186 infoText = "".join(info) 1187 1188 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1189 1190 else: 1191 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1192 1193 return prices 1194 1195 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1196 """ 1197 This method get and show information about all available broker instruments for current user account. 1198 If `instrumentsFile` string is not empty then also save information to this file. 1199 1200 :param show: if `True` then print results to console, if `False` — print only to file. 1201 :return: multi-lines string with all available broker instruments 1202 """ 1203 if not self.iList: 1204 self.iList = self.Listing() 1205 1206 info = [ 1207 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1208 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1209 ] 1210 1211 # add instruments count by type: 1212 for iType in self.iList.keys(): 1213 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1214 1215 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1216 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1217 1218 # generating info tables with all instruments by type: 1219 for iType in self.iList.keys(): 1220 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1221 1222 for instrument in self.iList[iType].keys(): 1223 iName = self.iList[iType][instrument]["name"] # instrument's name 1224 if len(iName) > 57: 1225 iName = "{}...".format(iName[:54]) # right trim for a long string 1226 1227 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1228 self.iList[iType][instrument]["ticker"], 1229 iName, 1230 self.iList[iType][instrument]["figi"], 1231 self.iList[iType][instrument]["currency"], 1232 self.iList[iType][instrument]["lot"], 1233 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1234 )) 1235 1236 infoText = "".join(info) 1237 1238 if show: 1239 uLogger.info(infoText) 1240 1241 if self.instrumentsFile: 1242 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1243 fH.write(infoText) 1244 1245 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1246 1247 if self.useHTMLReports: 1248 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1249 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1250 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1251 1252 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1253 1254 return infoText 1255 1256 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1257 """ 1258 This method search and show information about instruments by part of its ticker, FIGI or name. 1259 If `searchResultsFile` string is not empty then also save information to this file. 1260 1261 :param pattern: string with part of ticker, FIGI or instrument's name. 1262 :param show: if `True` then print results to console, if `False` — return list of result only. 1263 :return: list of dictionaries with all found instruments. 1264 """ 1265 if not self.iList: 1266 self.iList = self.Listing() 1267 1268 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1269 compiledPattern = re.compile(pattern, re.IGNORECASE) 1270 1271 for iType in self.iList: 1272 for instrument in self.iList[iType].values(): 1273 searchResult = compiledPattern.search(" ".join( 1274 [instrument["ticker"], instrument["figi"], instrument["name"]] 1275 )) 1276 1277 if searchResult: 1278 searchResults[iType][instrument["ticker"]] = instrument 1279 1280 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1281 info = [ 1282 "# Search results\n\n", 1283 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1284 "* **Search pattern:** [{}]\n".format(pattern), 1285 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1286 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1287 ] 1288 infoShort = info[:] 1289 1290 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1291 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1292 skippedLine = "| ... | ... | ... | ... |\n" 1293 1294 if resultsLen == 0: 1295 info.append("\nNo results\n") 1296 infoShort.append("\nNo results\n") 1297 uLogger.warning("No results. Try changing your search pattern.") 1298 1299 else: 1300 for iType in searchResults: 1301 iTypeValuesCount = len(searchResults[iType].values()) 1302 if iTypeValuesCount > 0: 1303 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1304 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1305 1306 for instrument in searchResults[iType].values(): 1307 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1308 instrument["type"], 1309 instrument["ticker"], 1310 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1311 instrument["figi"], 1312 )) 1313 1314 if iTypeValuesCount <= 5: 1315 infoShort.extend(info[-iTypeValuesCount:]) 1316 1317 else: 1318 infoShort.extend(info[-5:]) 1319 infoShort.append(skippedLine) 1320 1321 infoText = "".join(info) 1322 infoTextShort = "".join(infoShort) 1323 1324 if show: 1325 uLogger.info(infoTextShort) 1326 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1327 1328 if self.searchResultsFile: 1329 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1330 fH.write(infoText) 1331 1332 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1333 1334 if self.useHTMLReports: 1335 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1336 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1337 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1338 1339 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1340 1341 return searchResults 1342 1343 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1344 """ 1345 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1346 1347 :param instruments: list of strings with tickers or FIGIs. 1348 :return: list with unique instrument FIGIs only. 1349 """ 1350 requestedInstruments = [] 1351 for iName in instruments: 1352 if iName not in self.aliases.keys(): 1353 if iName not in requestedInstruments: 1354 requestedInstruments.append(iName) 1355 1356 else: 1357 if iName not in requestedInstruments: 1358 if self.aliases[iName] not in requestedInstruments: 1359 requestedInstruments.append(self.aliases[iName]) 1360 1361 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1362 1363 onlyUniqueFIGIs = [] 1364 for iName in requestedInstruments: 1365 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1366 continue 1367 1368 self._ticker = iName 1369 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1370 1371 if not iData: 1372 self._ticker = "" 1373 self._figi = iName 1374 1375 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1376 1377 if not iData: 1378 self._figi = "" 1379 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1380 1381 if iData and iData["figi"] not in onlyUniqueFIGIs: 1382 onlyUniqueFIGIs.append(iData["figi"]) 1383 1384 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1385 1386 return onlyUniqueFIGIs 1387 1388 def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]: 1389 """ 1390 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1391 1392 See limits: https://tinkoff.github.io/investAPI/limits/ 1393 1394 If `pricesFile` string is not empty then also save information to this file. 1395 1396 :param instruments: list of strings with tickers or FIGIs. 1397 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1398 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1399 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1400 """ 1401 if instruments is None or not instruments: 1402 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1403 raise Exception("Ticker or FIGI required") 1404 1405 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1406 1407 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1408 1409 iList = [] # trying to get info and current prices about all unique instruments: 1410 for self._figi in onlyUniqueFIGIs: 1411 iData = self.SearchByFIGI(requestPrice=True) 1412 iList.append(iData) 1413 1414 self.ShowListOfPrices(iList, show) 1415 1416 return iList 1417 1418 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1419 """ 1420 Show table contains current prices of given instruments. 1421 1422 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1423 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1424 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1425 :return: multilines text in Markdown format as a table contains current prices. 1426 """ 1427 infoText = "" 1428 1429 if show or self.pricesFile: 1430 info = [ 1431 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1432 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1433 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1434 ] 1435 1436 for item in iList: 1437 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1438 item["ticker"], 1439 item["figi"], 1440 item["type"], 1441 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1442 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1443 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1444 "{} / {}".format( 1445 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1446 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1447 ), 1448 "{} / {}".format( 1449 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1450 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1451 ), 1452 item["currency"], 1453 )) 1454 1455 infoText = "".join(info) 1456 1457 if show: 1458 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1459 1460 if self.pricesFile: 1461 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1462 fH.write(infoText) 1463 1464 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1465 1466 if self.useHTMLReports: 1467 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1468 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1469 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1470 1471 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1472 1473 return infoText 1474 1475 def RequestTradingStatus(self) -> dict: 1476 """ 1477 Requesting trading status for the instrument defined by `figi` variable. 1478 1479 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1480 1481 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1482 1483 :return: dictionary with trading status attributes. Response example: 1484 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1485 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1486 """ 1487 if self._figi is None or not self._figi: 1488 uLogger.error("Variable `figi` must be defined for using this method!") 1489 raise Exception("FIGI required") 1490 1491 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1492 1493 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1494 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1495 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1496 1497 if self.moreDebug: 1498 uLogger.debug("Records about current trading status successfully received") 1499 1500 return tradingStatus 1501 1502 def RequestPortfolio(self) -> dict: 1503 """ 1504 Requesting actual user's portfolio for current `accountId`. 1505 1506 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1507 1508 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1509 1510 :return: dictionary with user's portfolio. 1511 """ 1512 if self.accountId is None or not self.accountId: 1513 uLogger.error("Variable `accountId` must be defined for using this method!") 1514 raise Exception("Account ID required") 1515 1516 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1517 1518 self.body = str({"accountId": self.accountId}) 1519 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1520 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1521 1522 if self.moreDebug: 1523 uLogger.debug("Records about user's portfolio successfully received") 1524 1525 return rawPortfolio 1526 1527 def RequestPositions(self) -> dict: 1528 """ 1529 Requesting open positions by currencies and instruments for current `accountId`. 1530 1531 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1532 1533 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1534 1535 :return: dictionary with open positions by instruments. 1536 """ 1537 if self.accountId is None or not self.accountId: 1538 uLogger.error("Variable `accountId` must be defined for using this method!") 1539 raise Exception("Account ID required") 1540 1541 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1542 1543 self.body = str({"accountId": self.accountId}) 1544 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1545 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1546 1547 if self.moreDebug: 1548 uLogger.debug("Records about current open positions successfully received") 1549 1550 return rawPositions 1551 1552 def RequestPendingOrders(self) -> list: 1553 """ 1554 Requesting current actual pending limit orders for current `accountId`. 1555 1556 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1557 1558 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1559 1560 :return: list of dictionaries with pending limit orders. 1561 """ 1562 if self.accountId is None or not self.accountId: 1563 uLogger.error("Variable `accountId` must be defined for using this method!") 1564 raise Exception("Account ID required") 1565 1566 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1567 1568 self.body = str({"accountId": self.accountId}) 1569 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1570 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1571 1572 if "orders" in rawResponse.keys(): 1573 rawOrders = rawResponse["orders"] 1574 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1575 1576 else: 1577 rawOrders = [] 1578 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1579 1580 return rawOrders 1581 1582 def RequestStopOrders(self) -> list: 1583 """ 1584 Requesting current actual stop orders for current `accountId`. 1585 1586 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1587 1588 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1589 1590 :return: list of dictionaries with stop orders. 1591 """ 1592 if self.accountId is None or not self.accountId: 1593 uLogger.error("Variable `accountId` must be defined for using this method!") 1594 raise Exception("Account ID required") 1595 1596 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1597 1598 self.body = str({"accountId": self.accountId}) 1599 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1600 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1601 1602 if "stopOrders" in rawResponse.keys(): 1603 rawStopOrders = rawResponse["stopOrders"] 1604 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1605 1606 else: 1607 rawStopOrders = [] 1608 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1609 1610 return rawStopOrders 1611 1612 def Overview(self, show: bool = False, details: str = "full") -> dict: 1613 """ 1614 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1615 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1616 and `overviewBondsCalendarFile` are defined then also save information to file. 1617 1618 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1619 many requests about the state of the portfolio, and then, based on the received data, a large number 1620 of calculation and statistics are collected. 1621 1622 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1623 :param details: how detailed should the information be? 1624 - `full` — shows full available information about portfolio status (by default), 1625 - `positions` — shows only open positions, 1626 - `orders` — shows only sections of open limits and stop orders. 1627 - `digest` — show a short digest of the portfolio status, 1628 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1629 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1630 :return: dictionary with client's raw portfolio and some statistics. 1631 """ 1632 if self.accountId is None or not self.accountId: 1633 uLogger.error("Variable `accountId` must be defined for using this method!") 1634 raise Exception("Account ID required") 1635 1636 view = { 1637 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1638 "headers": {}, # list of dictionaries, response headers without "positions" section 1639 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1640 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1641 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1642 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1643 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1644 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1645 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1646 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1647 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1648 }, 1649 "stat": { # --- some statistics calculated using "raw" sections: 1650 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1651 "availableRUB": 0., # available rubles (without other currencies) 1652 "blockedRUB": 0., # blocked sum in Russian Rouble 1653 "totalChangesRUB": 0., # changes for all open trades in RUB 1654 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1655 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1656 "sharesCostRUB": 0., # costs of all shares in RUB 1657 "bondsCostRUB": 0., # costs of all bonds in RUB 1658 "etfsCostRUB": 0., # costs of all etfs in RUB 1659 "futuresCostRUB": 0., # costs of all futures in RUB 1660 "Currencies": [], # list of dictionaries of all currencies statistics 1661 "Shares": [], # list of dictionaries of all shares statistics 1662 "Bonds": [], # list of dictionaries of all bonds statistics 1663 "Etfs": [], # list of dictionaries of all etfs statistics 1664 "Futures": [], # list of dictionaries of all futures statistics 1665 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1666 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1667 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1668 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1669 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1670 }, 1671 "analytics": { # --- some analytics of portfolio: 1672 "distrByAssets": {}, # portfolio distribution by assets 1673 "distrByCompanies": {}, # portfolio distribution by companies 1674 "distrBySectors": {}, # portfolio distribution by sectors 1675 "distrByCurrencies": {}, # portfolio distribution by currencies 1676 "distrByCountries": {}, # portfolio distribution by countries 1677 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1678 } 1679 } 1680 1681 details = details.lower() 1682 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1683 if details not in availableDetails: 1684 details = "full" 1685 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1686 1687 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1688 1689 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1690 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1691 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1692 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1693 1694 # save response headers without "positions" section: 1695 for key in portfolioResponse.keys(): 1696 if key != "positions": 1697 view["raw"]["headers"][key] = portfolioResponse[key] 1698 1699 else: 1700 continue 1701 1702 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1703 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1704 for item in portfolioResponse["positions"]: 1705 if item["instrumentType"] == "currency": 1706 self._figi = item["figi"] 1707 if not self._figi and item["ticker"]: 1708 self._ticker = item["ticker"] 1709 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1710 1711 curr = self.SearchByFIGI(requestPrice=False) 1712 1713 # current price of currency in RUB: 1714 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1715 "name": curr["name"], 1716 "currentPrice": NanoToFloat( 1717 item["currentPrice"]["units"], 1718 item["currentPrice"]["nano"] 1719 ), 1720 } 1721 1722 view["raw"]["Currencies"].append(item) 1723 1724 elif item["instrumentType"] == "share": 1725 view["raw"]["Shares"].append(item) 1726 1727 elif item["instrumentType"] == "bond": 1728 view["raw"]["Bonds"].append(item) 1729 1730 elif item["instrumentType"] == "etf": 1731 view["raw"]["Etfs"].append(item) 1732 1733 elif item["instrumentType"] == "futures": 1734 view["raw"]["Futures"].append(item) 1735 1736 else: 1737 continue 1738 1739 # how many volume of currencies (by ISO currency name) are blocked: 1740 for item in view["raw"]["positions"]["blocked"]: 1741 blocked = NanoToFloat(item["units"], item["nano"]) 1742 if blocked > 0: 1743 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1744 1745 # how many volume of instruments (by FIGI) are blocked: 1746 for item in view["raw"]["positions"]["securities"]: 1747 blocked = int(item["blocked"]) 1748 if blocked > 0: 1749 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1750 1751 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1752 1753 if "rub" in allBlocked.keys(): 1754 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1755 1756 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1757 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1758 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1759 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1760 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1761 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1762 view["stat"]["portfolioCostRUB"] = sum([ 1763 view["stat"]["allCurrenciesCostRUB"], 1764 view["stat"]["sharesCostRUB"], 1765 view["stat"]["bondsCostRUB"], 1766 view["stat"]["etfsCostRUB"], 1767 view["stat"]["futuresCostRUB"], 1768 ]) 1769 1770 # --- calculating some portfolio statistics: 1771 byComp = {} # distribution by companies 1772 bySect = {} # distribution by sectors 1773 byCurr = {} # distribution by currencies (include RUB) 1774 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1775 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1776 1777 for item in portfolioResponse["positions"]: 1778 self._figi = item["figi"] 1779 if not self._figi and item["ticker"]: 1780 self._ticker = item["ticker"] 1781 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1782 1783 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1784 1785 if instrument: 1786 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1787 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1788 1789 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1790 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1791 1792 else: 1793 blocked = 0 1794 1795 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1796 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1797 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1798 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1799 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1800 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1801 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1802 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1803 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1804 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1805 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1806 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1807 1808 statData = { 1809 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1810 "ticker": instrument["ticker"], # ticker by FIGI 1811 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1812 "volume": volume, # available volume of instrument 1813 "lots": lots, # volume in lots of instrument 1814 "direction": direction, # direction of an instrument's position: short or long 1815 "blocked": blocked, # blocked volume of currency or instrument 1816 "currentPrice": curPrice, # current instrument's price in basic asset 1817 "average": average, # current average position price 1818 "cost": cost, # current cost of all volume of instrument in basic asset 1819 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1820 "costRUB": costRUB, # cost of instrument in ruble 1821 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1822 "profit": profit, # expected profit at current moment 1823 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1824 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1825 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1826 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1827 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1828 "step": instrument["step"], # minimum price increment 1829 } 1830 1831 # adding distribution by unique countries: 1832 if statData["country"] not in byCountry.keys(): 1833 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1834 1835 else: 1836 byCountry[statData["country"]]["cost"] += costRUB 1837 byCountry[statData["country"]]["percent"] += percentCostRUB 1838 1839 if item["instrumentType"] != "currency": 1840 # adding distribution by unique companies: 1841 if statData["name"]: 1842 if statData["name"] not in byComp.keys(): 1843 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1844 1845 else: 1846 byComp[statData["name"]]["cost"] += costRUB 1847 byComp[statData["name"]]["percent"] += percentCostRUB 1848 1849 # adding distribution by unique sectors: 1850 if statData["sector"] not in bySect.keys(): 1851 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1852 1853 else: 1854 bySect[statData["sector"]]["cost"] += costRUB 1855 bySect[statData["sector"]]["percent"] += percentCostRUB 1856 1857 # adding distribution by unique currencies: 1858 if currency not in byCurr.keys(): 1859 byCurr[currency] = { 1860 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1861 "cost": costRUB, 1862 "percent": percentCostRUB 1863 } 1864 1865 else: 1866 byCurr[currency]["cost"] += costRUB 1867 byCurr[currency]["percent"] += percentCostRUB 1868 1869 # saving statistics for every instrument: 1870 if item["instrumentType"] == "currency": 1871 view["stat"]["Currencies"].append(statData) 1872 1873 # update dict with free funds for trading (total - blocked) by currencies 1874 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1875 view["stat"]["funds"][currency] = { 1876 "total": volume, 1877 "totalCostRUB": costRUB, # total volume cost in rubles 1878 "free": volume - blocked, 1879 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1880 } 1881 1882 elif item["instrumentType"] == "share": 1883 view["stat"]["Shares"].append(statData) 1884 1885 elif item["instrumentType"] == "bond": 1886 view["stat"]["Bonds"].append(statData) 1887 1888 elif item["instrumentType"] == "etf": 1889 view["stat"]["Etfs"].append(statData) 1890 1891 elif item["instrumentType"] == "Futures": 1892 view["stat"]["Futures"].append(statData) 1893 1894 else: 1895 continue 1896 1897 # total changes in Russian Ruble: 1898 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1899 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1900 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1901 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1902 view["stat"]["funds"]["rub"] = { 1903 "total": view["stat"]["availableRUB"], 1904 "totalCostRUB": view["stat"]["availableRUB"], 1905 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1906 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1907 } 1908 1909 # --- pending limit orders sector data: 1910 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1911 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1912 1913 for item in view["raw"]["orders"]: 1914 self._figi = item["figi"] 1915 1916 if item["figi"] not in uniquePendingOrdersFIGIs: 1917 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1918 1919 uniquePendingOrdersFIGIs.append(item["figi"]) 1920 uniquePendingOrders[item["figi"]] = instrument 1921 1922 else: 1923 instrument = uniquePendingOrders[item["figi"]] 1924 1925 if instrument: 1926 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1927 orderType = TKS_ORDER_TYPES[item["orderType"]] 1928 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1929 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1930 1931 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1932 if item["direction"] == "ORDER_DIRECTION_BUY": 1933 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1934 1935 else: 1936 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1937 1938 # requested price for order execution: 1939 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1940 1941 # necessary changes in percent to reach target from current price: 1942 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1943 1944 view["stat"]["orders"].append({ 1945 "orderID": item["orderId"], # orderId number parameter of current order 1946 "figi": item["figi"], # FIGI identification 1947 "ticker": instrument["ticker"], # ticker name by FIGI 1948 "lotsRequested": item["lotsRequested"], # requested lots value 1949 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1950 "currentPrice": lastPrice, # current instrument's price for defined action 1951 "targetPrice": target, # requested price for order execution in base currency 1952 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1953 "percentChanges": changes, # changes in percent to target from current price 1954 "currency": item["currency"], # instrument's currency name 1955 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1956 "type": orderType, # type of order from TKS_ORDER_TYPES 1957 "status": orderState, # order status from TKS_ORDER_STATES 1958 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1959 }) 1960 1961 # --- stop orders sector data: 1962 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1963 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1964 1965 for item in view["raw"]["stopOrders"]: 1966 self._figi = item["figi"] 1967 1968 if item["figi"] not in uniqueStopOrdersFIGIs: 1969 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1970 1971 uniqueStopOrdersFIGIs.append(item["figi"]) 1972 uniqueStopOrders[item["figi"]] = instrument 1973 1974 else: 1975 instrument = uniqueStopOrders[item["figi"]] 1976 1977 if instrument: 1978 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1979 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1980 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1981 1982 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1983 if "expirationTime" in item.keys(): 1984 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1985 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1986 1987 else: 1988 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1989 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1990 1991 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1992 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1993 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1994 1995 else: 1996 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1997 1998 # requested price when stop-order executed: 1999 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2000 2001 # price for limit-order, set up when stop-order executed: 2002 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2003 2004 # necessary changes in percent to reach target from current price: 2005 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2006 2007 view["stat"]["stopOrders"].append({ 2008 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2009 "figi": item["figi"], # FIGI identification 2010 "ticker": instrument["ticker"], # ticker name by FIGI 2011 "lotsRequested": item["lotsRequested"], # requested lots value 2012 "currentPrice": lastPrice, # current instrument's price for defined action 2013 "targetPrice": target, # requested price for stop-order execution in base currency 2014 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2015 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2016 "percentChanges": changes, # changes in percent to target from current price 2017 "currency": item["currency"], # instrument's currency name 2018 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2019 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2020 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2021 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2022 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2023 }) 2024 2025 # --- calculating data for analytics section: 2026 # portfolio distribution by assets: 2027 view["analytics"]["distrByAssets"] = { 2028 "Ruble": { 2029 "uniques": 1, 2030 "cost": view["stat"]["availableRUB"], 2031 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2032 }, 2033 "Currencies": { 2034 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2035 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2036 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2037 }, 2038 "Shares": { 2039 "uniques": len(view["stat"]["Shares"]), 2040 "cost": view["stat"]["sharesCostRUB"], 2041 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2042 }, 2043 "Bonds": { 2044 "uniques": len(view["stat"]["Bonds"]), 2045 "cost": view["stat"]["bondsCostRUB"], 2046 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2047 }, 2048 "Etfs": { 2049 "uniques": len(view["stat"]["Etfs"]), 2050 "cost": view["stat"]["etfsCostRUB"], 2051 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2052 }, 2053 "Futures": { 2054 "uniques": len(view["stat"]["Futures"]), 2055 "cost": view["stat"]["futuresCostRUB"], 2056 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2057 }, 2058 } 2059 2060 # portfolio distribution by companies: 2061 view["analytics"]["distrByCompanies"]["All money cash"] = { 2062 "ticker": "", 2063 "cost": view["stat"]["allCurrenciesCostRUB"], 2064 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2065 } 2066 view["analytics"]["distrByCompanies"].update(byComp) 2067 2068 # portfolio distribution by sectors: 2069 view["analytics"]["distrBySectors"]["All money cash"] = { 2070 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2071 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2072 } 2073 view["analytics"]["distrBySectors"].update(bySect) 2074 2075 # portfolio distribution by currencies: 2076 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2077 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2078 2079 if self.moreDebug: 2080 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2081 2082 view["analytics"]["distrByCurrencies"].update(byCurr) 2083 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2084 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2085 2086 # portfolio distribution by countries: 2087 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2088 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2089 2090 if self.moreDebug: 2091 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2092 2093 view["analytics"]["distrByCountries"].update(byCountry) 2094 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2095 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2096 2097 # --- Prepare text statistics overview in human-readable: 2098 if show: 2099 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2100 2101 # Whatever the value `details`, header not changes: 2102 info = [ 2103 "# Client's portfolio\n\n", 2104 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2105 "* **Account ID:** [{}]\n".format(self.accountId), 2106 ] 2107 2108 if details in ["full", "positions", "digest"]: 2109 info.extend([ 2110 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2111 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2112 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2113 view["stat"]["totalChangesRUB"], 2114 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2115 view["stat"]["totalChangesPercentRUB"], 2116 ), 2117 ]) 2118 2119 if details in ["full", "positions"]: 2120 info.extend([ 2121 "## Open positions\n\n", 2122 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2123 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2124 "| **Ruble:** | {:>31} | | | | | |\n".format( 2125 "{:.2f} ({:.2f}) rub".format( 2126 view["stat"]["availableRUB"], 2127 view["stat"]["blockedRUB"], 2128 ) 2129 ) 2130 ]) 2131 2132 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2133 return [ 2134 "| | | | | | | |\n", 2135 "| {:<27} | | | | | {:>19} | |\n".format( 2136 noTradeStr if noTradeStr else typeStr, 2137 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2138 ), 2139 ] 2140 2141 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2142 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2143 "{} [{}]".format(data["ticker"], data["figi"]), 2144 "{:.2f} ({:.2f}) {}".format( 2145 data["volume"], 2146 data["blocked"], 2147 data["currency"], 2148 ) if isCurr else "{:.0f} ({:.0f})".format( 2149 data["volume"], 2150 data["blocked"], 2151 ), 2152 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2153 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2154 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2155 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2156 "{}{:.2f} {} ({}{:.2f}%)".format( 2157 "+" if data["profit"] > 0 else "", 2158 data["profit"], data["baseCurrencyName"], 2159 "+" if data["percentProfit"] > 0 else "", 2160 data["percentProfit"], 2161 ), 2162 ) 2163 2164 # --- Show currencies section: 2165 if view["stat"]["Currencies"]: 2166 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2167 for item in view["stat"]["Currencies"]: 2168 info.append(_InfoStr(item, isCurr=True)) 2169 2170 else: 2171 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2172 2173 # --- Show shares section: 2174 if view["stat"]["Shares"]: 2175 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2176 2177 for item in view["stat"]["Shares"]: 2178 info.append(_InfoStr(item)) 2179 2180 else: 2181 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2182 2183 # --- Show bonds section: 2184 if view["stat"]["Bonds"]: 2185 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2186 2187 for item in view["stat"]["Bonds"]: 2188 info.append(_InfoStr(item)) 2189 2190 else: 2191 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2192 2193 # --- Show etfs section: 2194 if view["stat"]["Etfs"]: 2195 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2196 2197 for item in view["stat"]["Etfs"]: 2198 info.append(_InfoStr(item)) 2199 2200 else: 2201 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2202 2203 # --- Show futures section: 2204 if view["stat"]["Futures"]: 2205 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2206 2207 for item in view["stat"]["Futures"]: 2208 info.append(_InfoStr(item)) 2209 2210 else: 2211 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2212 2213 if details in ["full", "orders"]: 2214 # --- Show pending limit orders section: 2215 if view["stat"]["orders"]: 2216 info.extend([ 2217 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2218 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2219 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2220 ]) 2221 2222 for item in view["stat"]["orders"]: 2223 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2224 "{} [{}]".format(item["ticker"], item["figi"]), 2225 item["orderID"], 2226 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2227 "{} {} ({}{:.2f}%)".format( 2228 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2229 item["baseCurrencyName"], 2230 "+" if item["percentChanges"] > 0 else "", 2231 float(item["percentChanges"]), 2232 ), 2233 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2234 item["action"], 2235 item["type"], 2236 item["date"], 2237 )) 2238 2239 else: 2240 info.append("\n## Total pending limit-orders: [0]\n") 2241 2242 # --- Show stop orders section: 2243 if view["stat"]["stopOrders"]: 2244 info.extend([ 2245 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2246 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2247 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2248 ]) 2249 2250 for item in view["stat"]["stopOrders"]: 2251 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2252 "{} [{}]".format(item["ticker"], item["figi"]), 2253 item["orderID"], 2254 item["lotsRequested"], 2255 "{} {} ({}{:.2f}%)".format( 2256 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2257 item["baseCurrencyName"], 2258 "+" if item["percentChanges"] > 0 else "", 2259 float(item["percentChanges"]), 2260 ), 2261 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2262 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2263 item["action"], 2264 item["type"], 2265 item["expType"], 2266 item["createDate"], 2267 item["expDate"], 2268 )) 2269 2270 else: 2271 info.append("\n## Total stop-orders: [0]\n") 2272 2273 if details in ["full", "analytics"]: 2274 # -- Show analytics section: 2275 if view["stat"]["portfolioCostRUB"] > 0: 2276 info.extend([ 2277 "\n# Analytics\n\n" 2278 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2279 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2280 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2281 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2282 view["stat"]["totalChangesRUB"], 2283 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2284 view["stat"]["totalChangesPercentRUB"], 2285 ), 2286 "\n## Portfolio distribution by assets\n" 2287 "\n| Type | Uniques | Percent | Current cost |\n", 2288 "|------------------------------------|---------|---------|--------------------|\n", 2289 ]) 2290 2291 for key in view["analytics"]["distrByAssets"].keys(): 2292 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2293 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2294 key, 2295 view["analytics"]["distrByAssets"][key]["uniques"], 2296 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2297 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2298 )) 2299 2300 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2301 2302 info.extend([ 2303 "\n## Portfolio distribution by companies\n" 2304 "\n| Company | Percent | Current cost |\n", 2305 aSepLine, 2306 ]) 2307 2308 for company in view["analytics"]["distrByCompanies"].keys(): 2309 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2310 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2311 "{}{}".format( 2312 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2313 company, 2314 ), 2315 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2316 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2317 )) 2318 2319 info.extend([ 2320 "\n## Portfolio distribution by sectors\n" 2321 "\n| Sector | Percent | Current cost |\n", 2322 aSepLine, 2323 ]) 2324 2325 for sector in view["analytics"]["distrBySectors"].keys(): 2326 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2327 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2328 sector, 2329 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2330 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2331 )) 2332 2333 info.extend([ 2334 "\n## Portfolio distribution by currencies\n" 2335 "\n| Instruments currencies | Percent | Current cost |\n", 2336 aSepLine, 2337 ]) 2338 2339 for curr in view["analytics"]["distrByCurrencies"].keys(): 2340 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2341 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2342 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2343 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2344 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2345 )) 2346 2347 info.extend([ 2348 "\n## Portfolio distribution by countries\n" 2349 "\n| Assets by country | Percent | Current cost |\n", 2350 aSepLine, 2351 ]) 2352 2353 for country in view["analytics"]["distrByCountries"].keys(): 2354 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2355 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2356 country, 2357 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2358 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2359 )) 2360 2361 if details in ["full", "calendar"]: 2362 # -- Show bonds payment calendar section: 2363 if view["stat"]["Bonds"]: 2364 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2365 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2366 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2367 2368 else: 2369 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2370 2371 infoText = "".join(info) 2372 2373 uLogger.info(infoText) 2374 2375 if details == "full" and self.overviewFile: 2376 filename = self.overviewFile 2377 2378 elif details == "digest" and self.overviewDigestFile: 2379 filename = self.overviewDigestFile 2380 2381 elif details == "positions" and self.overviewPositionsFile: 2382 filename = self.overviewPositionsFile 2383 2384 elif details == "orders" and self.overviewOrdersFile: 2385 filename = self.overviewOrdersFile 2386 2387 elif details == "analytics" and self.overviewAnalyticsFile: 2388 filename = self.overviewAnalyticsFile 2389 2390 elif details == "calendar" and self.overviewBondsCalendarFile: 2391 filename = self.overviewBondsCalendarFile 2392 2393 else: 2394 filename = "" 2395 2396 if filename: 2397 with open(filename, "w", encoding="UTF-8") as fH: 2398 fH.write(infoText) 2399 2400 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2401 2402 if self.useHTMLReports: 2403 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2404 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2405 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2406 2407 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2408 2409 return view 2410 2411 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2412 """ 2413 Returns history operations between two given dates for current `accountId`. 2414 If `reportFile` string is not empty then also save human-readable report. 2415 Shows some statistical data of closed positions. 2416 2417 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2418 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2419 :param show: if `True` then also prints all records to the console. 2420 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2421 :return: original list of dictionaries with history of deals records from API ("operations" key): 2422 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2423 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2424 """ 2425 if self.accountId is None or not self.accountId: 2426 uLogger.error("Variable `accountId` must be defined for using this method!") 2427 raise Exception("Account ID required") 2428 2429 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2430 2431 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2432 2433 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2434 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2435 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2436 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2437 customStat = {} # custom statistics in additional to responseJSON 2438 2439 # --- output report in human-readable format: 2440 if show or self.reportFile: 2441 splitLine1 = "| | | | | |\n" # Summary section 2442 splitLine2 = "| | | | | | | | |\n" # Operations section 2443 nextDay = "" 2444 2445 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2446 2447 if len(ops) > 0: 2448 customStat = { 2449 "opsCount": 0, # total operations count 2450 "buyCount": 0, # buy operations 2451 "sellCount": 0, # sell operations 2452 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2453 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2454 "payIn": {"rub": 0.}, # Deposit brokerage account 2455 "payOut": {"rub": 0.}, # Withdrawals 2456 "divs": {"rub": 0.}, # Dividends income 2457 "coupons": {"rub": 0.}, # Coupon's income 2458 "brokerCom": {"rub": 0.}, # Service commissions 2459 "serviceCom": {"rub": 0.}, # Service commissions 2460 "marginCom": {"rub": 0.}, # Margin commissions 2461 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2462 } 2463 2464 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2465 for item in ops: 2466 if item["state"] == "OPERATION_STATE_EXECUTED": 2467 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2468 2469 # count buy operations: 2470 if "_BUY" in item["operationType"]: 2471 customStat["buyCount"] += 1 2472 2473 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2474 customStat["buyTotal"][item["payment"]["currency"]] += payment 2475 2476 else: 2477 customStat["buyTotal"][item["payment"]["currency"]] = payment 2478 2479 # count sell operations: 2480 elif "_SELL" in item["operationType"]: 2481 customStat["sellCount"] += 1 2482 2483 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2484 customStat["sellTotal"][item["payment"]["currency"]] += payment 2485 2486 else: 2487 customStat["sellTotal"][item["payment"]["currency"]] = payment 2488 2489 # count incoming operations: 2490 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2491 if item["payment"]["currency"] in customStat["payIn"].keys(): 2492 customStat["payIn"][item["payment"]["currency"]] += payment 2493 2494 else: 2495 customStat["payIn"][item["payment"]["currency"]] = payment 2496 2497 # count withdrawals operations: 2498 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2499 if item["payment"]["currency"] in customStat["payOut"].keys(): 2500 customStat["payOut"][item["payment"]["currency"]] += payment 2501 2502 else: 2503 customStat["payOut"][item["payment"]["currency"]] = payment 2504 2505 # count dividends income: 2506 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2507 if item["payment"]["currency"] in customStat["divs"].keys(): 2508 customStat["divs"][item["payment"]["currency"]] += payment 2509 2510 else: 2511 customStat["divs"][item["payment"]["currency"]] = payment 2512 2513 # count coupon's income: 2514 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2515 if item["payment"]["currency"] in customStat["coupons"].keys(): 2516 customStat["coupons"][item["payment"]["currency"]] += payment 2517 2518 else: 2519 customStat["coupons"][item["payment"]["currency"]] = payment 2520 2521 # count broker commissions: 2522 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2523 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2524 customStat["brokerCom"][item["payment"]["currency"]] += payment 2525 2526 else: 2527 customStat["brokerCom"][item["payment"]["currency"]] = payment 2528 2529 # count service commissions: 2530 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2531 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2532 customStat["serviceCom"][item["payment"]["currency"]] += payment 2533 2534 else: 2535 customStat["serviceCom"][item["payment"]["currency"]] = payment 2536 2537 # count margin commissions: 2538 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2539 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2540 customStat["marginCom"][item["payment"]["currency"]] += payment 2541 2542 else: 2543 customStat["marginCom"][item["payment"]["currency"]] = payment 2544 2545 # count withholding taxes: 2546 elif "_TAX" in item["operationType"]: 2547 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2548 customStat["allTaxes"][item["payment"]["currency"]] += payment 2549 2550 else: 2551 customStat["allTaxes"][item["payment"]["currency"]] = payment 2552 2553 else: 2554 continue 2555 2556 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2557 2558 # --- view "Actions" lines: 2559 info.extend([ 2560 "| Report sections | | | | |\n", 2561 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2562 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2563 "| | Buy: {:<22} | {:<28} | | |\n".format( 2564 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2565 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2566 ), 2567 "| | Sell: {:<21} | {:<28} | | |\n".format( 2568 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2569 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2570 ), 2571 ]) 2572 2573 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2574 for key in opsKeys: 2575 if key == "rub": 2576 continue 2577 2578 info.extend([ 2579 "| | | {:<28} | | |\n".format( 2580 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2581 ), 2582 "| | | {:<28} | | |\n".format( 2583 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2584 ), 2585 ]) 2586 2587 info.append(splitLine1) 2588 2589 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2590 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2591 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2592 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2593 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2594 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2595 ) 2596 2597 # --- view "Payments" lines: 2598 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2599 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2600 2601 for key in paymentsKeys: 2602 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2603 2604 info.append(splitLine1) 2605 2606 # --- view "Commissions and taxes" lines: 2607 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2608 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2609 2610 for key in comKeys: 2611 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2612 2613 info.extend([ 2614 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2615 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2616 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2617 ]) 2618 2619 else: 2620 info.append("Broker returned no operations during this period\n") 2621 2622 # --- view "Operations" section: 2623 for item in ops: 2624 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2625 continue 2626 2627 else: 2628 self._figi = item["figi"] 2629 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2630 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2631 2632 # group of deals during one day: 2633 if nextDay and item["date"].split("T")[0] != nextDay: 2634 info.append(splitLine2) 2635 nextDay = "" 2636 2637 else: 2638 nextDay = item["date"].split("T")[0] # saving current day for splitting 2639 2640 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2641 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2642 self._figi if self._figi else "—", 2643 instrument["ticker"] if instrument else "—", 2644 instrument["type"] if instrument else "—", 2645 item["quantity"] if int(item["quantity"]) > 0 else "—", 2646 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2647 TKS_OPERATION_STATES[item["state"]], 2648 TKS_OPERATION_TYPES[item["operationType"]], 2649 )) 2650 2651 infoText = "".join(info) 2652 2653 if show: 2654 if self.moreDebug: 2655 uLogger.debug("Records about history of a client's operations successfully received") 2656 2657 uLogger.info(infoText) 2658 2659 if self.reportFile: 2660 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2661 fH.write(infoText) 2662 2663 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2664 2665 if self.useHTMLReports: 2666 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2667 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2668 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2669 2670 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2671 2672 return ops, customStat 2673 2674 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2675 """ 2676 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2677 2678 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2679 Warning! Broker server used ISO UTC time by default. 2680 2681 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2682 Also, `historyFile` used to update history with `onlyMissing` parameter. 2683 2684 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2685 2686 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2687 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2688 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2689 `"hour"`, `"day"`. Default: `"hour"`. 2690 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2691 False by default. Warning! History appends only from last candle to current time 2692 with always update last candle! 2693 :param csvSep: separator if csv-file is used, `,` by default. 2694 :param show: if `True` then also prints Pandas DataFrame to the console. 2695 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2696 `["date", "time", "open", "high", "low", "close", "volume"]`. 2697 """ 2698 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2699 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2700 history = None # empty pandas object for history 2701 2702 if interval not in TKS_CANDLE_INTERVALS.keys(): 2703 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2704 raise Exception("Incorrect value") 2705 2706 if not (self._ticker or self._figi): 2707 uLogger.error("Ticker or FIGI must be defined!") 2708 raise Exception("Ticker or FIGI required") 2709 2710 if self._ticker and not self._figi: 2711 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2712 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2713 2714 if self._figi and not self._ticker: 2715 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2716 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2717 2718 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2719 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2720 if interval.lower() != "day": 2721 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2722 2723 delta = dtEnd - dtStart # current UTC time minus last time in file 2724 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2725 2726 # calculate history length in candles: 2727 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2728 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2729 length += 1 # to avoid fraction time 2730 2731 # calculate data blocks count: 2732 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2733 2734 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2735 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2736 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2737 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2738 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2739 2740 tempOld = None # pandas object for old history, if --only-missing key present 2741 lastTime = None # datetime object of last old candle in file 2742 2743 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2744 uLogger.debug("--only-missing key present, add only last missing candles...") 2745 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2746 2747 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2748 2749 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2750 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2751 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2752 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2753 2754 # get last datetime object from last string in file or minus 1 delta if file is empty: 2755 if len(tempOld) > 0: 2756 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2757 2758 else: 2759 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2760 2761 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2762 2763 responseJSONs = [] # raw history blocks of data 2764 2765 blockEnd = dtEnd 2766 for item in range(blocks): 2767 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2768 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2769 2770 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2771 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2772 )) 2773 2774 if blockStart == blockEnd: 2775 uLogger.debug("Skipped this zero-length block...") 2776 2777 else: 2778 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2779 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2780 self.body = str({ 2781 "figi": self._figi, 2782 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2783 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2784 "interval": TKS_CANDLE_INTERVALS[interval][0] 2785 }) 2786 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2787 2788 if "code" in responseJSON.keys(): 2789 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2790 2791 else: 2792 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2793 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2794 2795 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2796 2797 blockEnd = blockStart 2798 2799 printCount = len(responseJSONs) # candles to show in console 2800 if responseJSONs: 2801 tempHistory = pd.DataFrame( 2802 data={ 2803 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2804 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2805 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2806 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2807 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2808 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2809 "volume": [int(item["volume"]) for item in responseJSONs], 2810 }, 2811 index=range(len(responseJSONs)), 2812 columns=["date", "time", "open", "high", "low", "close", "volume"], 2813 ) 2814 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2815 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2816 2817 # append only newest candles to old history if --only-missing key present: 2818 if onlyMissing and tempOld is not None and lastTime is not None: 2819 index = 0 # find start index in tempHistory data: 2820 2821 for i, item in tempHistory.iterrows(): 2822 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2823 2824 if curTime == lastTime: 2825 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2826 index = i 2827 printCount = index + 1 2828 break 2829 2830 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2831 2832 else: 2833 history = tempHistory # if no `--only-missing` key then load full data from server 2834 2835 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2836 2837 if history is not None and not history.empty: 2838 if show: 2839 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2840 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2841 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2842 )) 2843 2844 else: 2845 uLogger.warning("Received an empty candles history!") 2846 2847 if self.historyFile is not None: 2848 if history is not None and not history.empty: 2849 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2850 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2851 2852 else: 2853 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2854 2855 else: 2856 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2857 2858 return history 2859 2860 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2861 """ 2862 Load candles history from csv-file and return Pandas DataFrame object. 2863 2864 See also: `History()` and `ShowHistoryChart()` methods. 2865 2866 :param filePath: path to csv-file to open. 2867 """ 2868 loadedHistory = None # init candles data object 2869 2870 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2871 2872 if os.path.exists(filePath): 2873 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2874 2875 tfStr = self.priceModel.FormattedDelta( 2876 self.priceModel.timeframe, 2877 "{days} days {hours}h {minutes}m {seconds}s", 2878 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2879 self.priceModel.timeframe, 2880 "{hours}h {minutes}m {seconds}s", 2881 ) 2882 2883 if loadedHistory is not None and not loadedHistory.empty: 2884 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2885 len(loadedHistory), 2886 tfStr, 2887 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2888 ) 2889 2890 else: 2891 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2892 2893 else: 2894 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2895 2896 return loadedHistory 2897 2898 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2899 """ 2900 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2901 2902 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2903 Default: `index.html` (both for interact and non-interact candlesticks chart). 2904 2905 See also: `History()` and `LoadHistory()` methods. 2906 2907 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2908 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2909 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2910 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2911 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2912 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2913 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2914 """ 2915 if isinstance(candles, str): 2916 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2917 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2918 2919 elif isinstance(candles, pd.DataFrame): 2920 self.priceModel.prices = candles # set candles chain from variable 2921 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2922 2923 if "datetime" not in candles.columns: 2924 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2925 2926 else: 2927 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2928 raise Exception("Incorrect value") 2929 2930 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2931 2932 if interact: 2933 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2934 2935 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2936 2937 else: 2938 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2939 2940 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2941 2942 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2943 2944 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2945 """ 2946 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2947 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2948 2949 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2950 2951 :param operation: string "Buy" or "Sell". 2952 :param lots: volume, integer count of lots >= 1. 2953 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2954 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2955 :param expDate: string "Undefined" by default or local date in future, 2956 it is a string with format `%Y-%m-%d %H:%M:%S`. 2957 :return: JSON with response from broker server. 2958 """ 2959 if self.accountId is None or not self.accountId: 2960 uLogger.error("Variable `accountId` must be defined for using this method!") 2961 raise Exception("Account ID required") 2962 2963 if operation is None or not operation or operation not in ("Buy", "Sell"): 2964 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2965 raise Exception("Incorrect value") 2966 2967 if lots is None or lots < 1: 2968 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2969 lots = 1 2970 2971 if tp is None or tp < 0: 2972 tp = 0 2973 2974 if sl is None or sl < 0: 2975 sl = 0 2976 2977 if expDate is None or not expDate: 2978 expDate = "Undefined" 2979 2980 if not (self._ticker or self._figi): 2981 uLogger.error("Ticker or FIGI must be defined!") 2982 raise Exception("Ticker or FIGI required") 2983 2984 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2985 self._ticker = instrument["ticker"] 2986 self._figi = instrument["figi"] 2987 2988 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 2989 2990 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2991 self.body = str({ 2992 "figi": self._figi, 2993 "quantity": str(lots), 2994 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2995 "accountId": str(self.accountId), 2996 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2997 }) 2998 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2999 3000 if "orderId" in response.keys(): 3001 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3002 operation, response["orderId"], 3003 self._ticker, self._figi, lots, 3004 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3005 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3006 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3007 )) 3008 3009 if tp > 0: 3010 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3011 3012 if sl > 0: 3013 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3014 3015 else: 3016 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3017 3018 return response 3019 3020 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3021 """ 3022 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3023 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3024 3025 See also: `Order()` and `Trade()` docstrings. 3026 3027 :param lots: volume, integer count of lots >= 1. 3028 :param tp: float > 0, take profit price of stop-order. 3029 :param sl: float > 0, stop loss price of stop-order. 3030 :param expDate: it's a local date in future. 3031 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3032 :return: JSON with response from broker server. 3033 """ 3034 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3035 3036 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3037 """ 3038 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3039 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3040 3041 See also: `Order()` and `Trade()` docstrings. 3042 3043 :param lots: volume, integer count of lots >= 1. 3044 :param tp: float > 0, take profit price of stop-order. 3045 :param sl: float > 0, stop loss price of stop-order. 3046 :param expDate: it's a local date in the future. 3047 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3048 :return: JSON with response from broker server. 3049 """ 3050 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3051 3052 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3053 """ 3054 Close position of given instruments. 3055 3056 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3057 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3058 This avoids unnecessary downloading data from the server. 3059 """ 3060 if instruments is None or not instruments: 3061 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3062 raise Exception("Ticker or FIGI required") 3063 3064 if isinstance(instruments, str): 3065 instruments = [instruments] 3066 3067 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3068 if uniqueInstruments: 3069 if portfolio is None or not portfolio: 3070 portfolio = self.Overview(show=False) 3071 3072 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3073 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3074 3075 for self._figi in uniqueInstruments: 3076 if self._figi not in allOpened: 3077 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3078 continue 3079 3080 # search open trade info about instrument by ticker: 3081 instrument = {} 3082 for iType in TKS_INSTRUMENTS: 3083 if instrument: 3084 break 3085 3086 for item in portfolio["stat"][iType]: 3087 if item["figi"] == self._figi: 3088 instrument = item 3089 break 3090 3091 if instrument: 3092 self._ticker = instrument["ticker"] 3093 self._figi = instrument["figi"] 3094 3095 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3096 self._ticker, 3097 self._figi, 3098 int(instrument["volume"]), 3099 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3100 )) 3101 3102 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3103 3104 if tradeLots > 0: 3105 if instrument["blocked"] > 0: 3106 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3107 instrument["blocked"], 3108 self._ticker, 3109 tradeLots, 3110 )) 3111 3112 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3113 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3114 3115 else: 3116 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3117 3118 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3119 """ 3120 Close all positions of given instruments with defined type. 3121 3122 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3123 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3124 This avoids unnecessary downloading data from the server. 3125 """ 3126 if iType not in TKS_INSTRUMENTS: 3127 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3128 3129 else: 3130 if portfolio is None or not portfolio: 3131 portfolio = self.Overview(show=False) 3132 3133 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3134 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3135 3136 if tickers and portfolio: 3137 self.CloseTrades(tickers, portfolio) 3138 3139 else: 3140 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3141 3142 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3143 """ 3144 Universal method to create market or limit orders with all available parameters for current `accountId`. 3145 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3146 3147 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3148 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3149 3150 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3151 then broker immediately open market order as you can do simple --buy or --sell operations! 3152 3153 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3154 When current price will go up or down to target price value then broker opens a limit order. 3155 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3156 3157 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3158 3159 :param operation: string "Buy" or "Sell". 3160 :param orderType: string "Limit" or "Stop". 3161 :param lots: volume, integer count of lots >= 1. 3162 :param targetPrice: target price > 0. This is open trade price for limit order. 3163 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3164 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3165 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3166 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3167 Stop loss order always executed by market price. 3168 :param expDate: string "Undefined" by default or local date in future. 3169 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3170 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3171 A limit order has no expiration date, it lasts until the end of the trading day. 3172 :return: JSON with response from broker server. 3173 """ 3174 if self.accountId is None or not self.accountId: 3175 uLogger.error("Variable `accountId` must be defined for using this method!") 3176 raise Exception("Account ID required") 3177 3178 if operation is None or not operation or operation not in ("Buy", "Sell"): 3179 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3180 raise Exception("Incorrect value") 3181 3182 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3183 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3184 raise Exception("Incorrect value") 3185 3186 if lots is None or lots < 1: 3187 uLogger.error("You must define trade volume > 0: integer count of lots!") 3188 raise Exception("Incorrect value") 3189 3190 if targetPrice is None or targetPrice <= 0: 3191 uLogger.error("Target price for limit-order must be greater than 0!") 3192 raise Exception("Incorrect value") 3193 3194 if limitPrice is None or limitPrice <= 0: 3195 limitPrice = targetPrice 3196 3197 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3198 stopType = "Limit" 3199 3200 if expDate is None or not expDate: 3201 expDate = "Undefined" 3202 3203 if not (self._ticker or self._figi): 3204 uLogger.error("Tocker or FIGI must be defined!") 3205 raise Exception("Ticker or FIGI required") 3206 3207 response = {} 3208 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3209 self._ticker = instrument["ticker"] 3210 self._figi = instrument["figi"] 3211 3212 if orderType == "Limit": 3213 uLogger.debug( 3214 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3215 self._ticker, self._figi, 3216 operation, lots, targetPrice, instrument["currency"], 3217 )) 3218 3219 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3220 self.body = str({ 3221 "figi": self._figi, 3222 "quantity": str(lots), 3223 "price": FloatToNano(targetPrice), 3224 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3225 "accountId": str(self.accountId), 3226 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3227 }) 3228 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3229 3230 if "orderId" in response.keys(): 3231 uLogger.info( 3232 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3233 response["orderId"], self._ticker, self._figi, operation, lots, 3234 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3235 )) 3236 3237 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3238 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3239 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3240 targetPrice, instrument["currency"], 3241 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3242 )) 3243 3244 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3245 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3246 targetPrice, instrument["currency"], 3247 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3248 )) 3249 3250 else: 3251 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3252 3253 if orderType == "Stop": 3254 uLogger.debug( 3255 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3256 self._ticker, self._figi, 3257 operation, lots, 3258 targetPrice, instrument["currency"], 3259 limitPrice, instrument["currency"], 3260 stopType, expDate, 3261 )) 3262 3263 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3264 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3265 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3266 3267 body = { 3268 "figi": self._figi, 3269 "quantity": str(lots), 3270 "price": FloatToNano(limitPrice), 3271 "stopPrice": FloatToNano(targetPrice), 3272 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3273 "accountId": str(self.accountId), 3274 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3275 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3276 } 3277 3278 if expDateUTC: 3279 body["expireDate"] = expDateUTC 3280 3281 self.body = str(body) 3282 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3283 3284 if "stopOrderId" in response.keys(): 3285 uLogger.info( 3286 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3287 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3288 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3289 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3290 TKS_STOP_ORDER_TYPES[stopOrderType], 3291 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3292 )) 3293 3294 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3295 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3296 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3297 targetPrice, instrument["currency"], 3298 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3299 )) 3300 3301 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3302 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3303 targetPrice, instrument["currency"], 3304 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3305 )) 3306 3307 else: 3308 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3309 3310 return response 3311 3312 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3313 """ 3314 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3315 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3316 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3317 See also: `Order()` docstring. 3318 3319 :param lots: volume, integer count of lots >= 1. 3320 :param targetPrice: target price > 0. This is open trade price for limit order. 3321 :return: JSON with response from broker server. 3322 """ 3323 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3324 3325 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3326 """ 3327 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3328 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3329 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3330 target price value then broker opens a limit order. See also: `Order()` docstring. 3331 3332 :param lots: volume, integer count of lots >= 1. 3333 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3334 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3335 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3336 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3337 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3338 :param expDate: string "Undefined" by default or local date in future. 3339 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3340 This date is converting to UTC format for server. 3341 :return: JSON with response from broker server. 3342 """ 3343 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3344 3345 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3346 """ 3347 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3348 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3349 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3350 See also: `Order()` docstring. 3351 3352 :param lots: volume, integer count of lots >= 1. 3353 :param targetPrice: target price > 0. This is open trade price for limit order. 3354 :return: JSON with response from broker server. 3355 """ 3356 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3357 3358 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3359 """ 3360 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3361 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3362 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3363 target price value then broker opens a limit order. See also: `Order()` docstring. 3364 3365 :param lots: volume, integer count of lots >= 1. 3366 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3367 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3368 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3369 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3370 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3371 :param expDate: string "Undefined" by default or local date in future. 3372 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3373 This date is converting to UTC format for server. 3374 :return: JSON with response from broker server. 3375 """ 3376 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3377 3378 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3379 """ 3380 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3381 3382 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3383 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3384 This avoids unnecessary downloading data from the server. 3385 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3386 """ 3387 if self.accountId is None or not self.accountId: 3388 uLogger.error("Variable `accountId` must be defined for using this method!") 3389 raise Exception("Account ID required") 3390 3391 if orderIDs: 3392 if allOrdersIDs is None: 3393 rawOrders = self.RequestPendingOrders() 3394 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3395 3396 if allStopOrdersIDs is None: 3397 rawStopOrders = self.RequestStopOrders() 3398 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3399 3400 for orderID in orderIDs: 3401 idInPendingOrders = orderID in allOrdersIDs 3402 idInStopOrders = orderID in allStopOrdersIDs 3403 3404 if not (idInPendingOrders or idInStopOrders): 3405 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3406 continue 3407 3408 else: 3409 if idInPendingOrders: 3410 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3411 3412 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3413 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3414 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3415 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3416 3417 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3418 if self.moreDebug: 3419 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3420 3421 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3422 3423 else: 3424 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3425 3426 elif idInStopOrders: 3427 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3428 3429 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3430 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3431 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3432 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3433 3434 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3435 if self.moreDebug: 3436 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3437 3438 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3439 3440 else: 3441 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3442 3443 else: 3444 continue 3445 3446 def CloseAllOrders(self) -> None: 3447 """ 3448 Gets a list of open pending and stop orders and cancel it all. 3449 """ 3450 rawOrders = self.RequestPendingOrders() 3451 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3452 lenOrders = len(allOrdersIDs) 3453 3454 rawStopOrders = self.RequestStopOrders() 3455 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3456 lenSOrders = len(allStopOrdersIDs) 3457 3458 if lenOrders > 0 or lenSOrders > 0: 3459 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3460 3461 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3462 3463 else: 3464 uLogger.info("Orders not found, nothing to cancel.") 3465 3466 def CloseAll(self, *args) -> None: 3467 """ 3468 Close all available (not blocked) opened trades and orders. 3469 3470 Also, you can select one or more keywords case-insensitive: 3471 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3472 3473 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3474 """ 3475 overview = self.Overview(show=False) # get all open trades info 3476 3477 if len(args) == 0: 3478 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3479 self.CloseAllOrders() # close all pending and stop orders 3480 3481 for iType in TKS_INSTRUMENTS: 3482 if iType != "Currencies": 3483 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3484 3485 else: 3486 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3487 lowerArgs = [x.lower() for x in args] 3488 3489 if "orders" in lowerArgs: 3490 self.CloseAllOrders() # close all pending and stop orders 3491 3492 for iType in TKS_INSTRUMENTS: 3493 if iType.lower() in lowerArgs and iType != "Currencies": 3494 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3495 3496 def CloseAllByTicker(self, instrument: str) -> None: 3497 """ 3498 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3499 3500 This method searches opened trade and orders of instrument throw all portfolio and then use 3501 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3502 3503 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3504 3505 :param instrument: string with ticker. 3506 """ 3507 if instrument is None or not instrument: 3508 uLogger.error("Ticker name must be defined for using this method!") 3509 raise Exception("Ticker required") 3510 3511 overview = self.Overview(show=False) # get user portfolio with all open trades info 3512 3513 self._ticker = instrument # try to set instrument as ticker 3514 self._figi = "" 3515 3516 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3517 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3518 3519 if limitAll and self.IsInLimitOrders(portfolio=overview): 3520 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3521 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3522 3523 if stopAll and self.IsInStopOrders(portfolio=overview): 3524 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3525 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3526 3527 if self.IsInPortfolio(portfolio=overview): 3528 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3529 self.CloseTrades(instruments=[instrument], portfolio=overview) 3530 3531 def CloseAllByFIGI(self, instrument: str) -> None: 3532 """ 3533 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3534 3535 This method searches opened trade and orders of instrument throw all portfolio and then use 3536 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3537 3538 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3539 3540 :param instrument: string with FIGI id. 3541 """ 3542 if instrument is None or not instrument: 3543 uLogger.error("FIGI id must be defined for using this method!") 3544 raise Exception("FIGI required") 3545 3546 overview = self.Overview(show=False) # get user portfolio with all open trades info 3547 3548 self._ticker = "" 3549 self._figi = instrument # try to set instrument as FIGI id 3550 3551 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3552 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3553 3554 if limitAll and self.IsInLimitOrders(portfolio=overview): 3555 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3556 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3557 3558 if stopAll and self.IsInStopOrders(portfolio=overview): 3559 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3560 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3561 3562 if self.IsInPortfolio(portfolio=overview): 3563 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3564 self.CloseTrades(instruments=[instrument], portfolio=overview) 3565 3566 @staticmethod 3567 def ParseOrderParameters(operation, **inputParameters): 3568 """ 3569 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3570 3571 :param operation: string "Buy" or "Sell". 3572 :param inputParameters: this is dict of strings that looks like this 3573 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3574 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3575 "prices" key: one or more prices to open limit-orders 3576 Counts of values in lots and prices lists must be equals! 3577 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3578 """ 3579 # TODO: update order grid work with api v2 3580 pass 3581 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3582 # 3583 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3584 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3585 # raise Exception("Incorrect value") 3586 # 3587 # if "l" in inputParameters.keys(): 3588 # inputParameters["lots"] = inputParameters.pop("l") 3589 # 3590 # if "p" in inputParameters.keys(): 3591 # inputParameters["prices"] = inputParameters.pop("p") 3592 # 3593 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3594 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3595 # raise Exception("Incorrect value") 3596 # 3597 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3598 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3599 # 3600 # if len(lots) != len(prices): 3601 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3602 # raise Exception("Incorrect value") 3603 # 3604 # uLogger.debug("Extracted parameters for orders:") 3605 # uLogger.debug("lots = {}".format(lots)) 3606 # uLogger.debug("prices = {}".format(prices)) 3607 # 3608 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3609 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3610 # uLogger.debug("Order parameters: {}".format(result)) 3611 # 3612 # return result 3613 3614 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3615 """ 3616 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3617 3618 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3619 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3620 """ 3621 result = False 3622 msg = "Instrument not defined!" 3623 3624 if portfolio is None or not portfolio: 3625 portfolio = self.Overview(show=False) 3626 3627 if self._ticker: 3628 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3629 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3630 3631 for iType in TKS_INSTRUMENTS: 3632 for instrument in portfolio["stat"][iType]: 3633 if instrument["ticker"] == self._ticker: 3634 result = True 3635 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3636 break 3637 3638 elif self._figi: 3639 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3640 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3641 3642 for iType in TKS_INSTRUMENTS: 3643 for instrument in portfolio["stat"][iType]: 3644 if instrument["figi"] == self._figi: 3645 result = True 3646 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3647 break 3648 3649 else: 3650 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3651 3652 uLogger.debug(msg) 3653 3654 return result 3655 3656 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3657 """ 3658 Returns instrument from the user's portfolio if it presents there. 3659 Instrument must be defined by `ticker` (highly priority) or `figi`. 3660 3661 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3662 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3663 """ 3664 result = None 3665 msg = "Instrument not defined!" 3666 3667 if portfolio is None or not portfolio: 3668 portfolio = self.Overview(show=False) 3669 3670 if self._ticker: 3671 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3672 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3673 3674 for iType in TKS_INSTRUMENTS: 3675 for instrument in portfolio["stat"][iType]: 3676 if instrument["ticker"] == self._ticker: 3677 result = instrument 3678 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3679 break 3680 3681 elif self._figi: 3682 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3683 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3684 3685 for iType in TKS_INSTRUMENTS: 3686 for instrument in portfolio["stat"][iType]: 3687 if instrument["figi"] == self._figi: 3688 result = instrument 3689 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3690 break 3691 3692 else: 3693 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3694 3695 uLogger.debug(msg) 3696 3697 return result 3698 3699 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3700 """ 3701 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3702 3703 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3704 3705 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3706 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3707 """ 3708 result = False 3709 msg = "Instrument not defined!" 3710 3711 if portfolio is None or not portfolio: 3712 portfolio = self.Overview(show=False) 3713 3714 if self._ticker: 3715 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3716 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3717 3718 for instrument in portfolio["stat"]["orders"]: 3719 if instrument["ticker"] == self._ticker: 3720 result = True 3721 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3722 break 3723 3724 elif self._figi: 3725 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3726 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3727 3728 for instrument in portfolio["stat"]["orders"]: 3729 if instrument["figi"] == self._figi: 3730 result = True 3731 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3732 break 3733 3734 else: 3735 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3736 3737 uLogger.debug(msg) 3738 3739 return result 3740 3741 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3742 """ 3743 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3744 Instrument must be defined by `ticker` (highly priority) or `figi`. 3745 3746 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3747 3748 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3749 :return: list with `orderID`s of limit orders. 3750 """ 3751 result = [] 3752 msg = "Instrument not defined!" 3753 3754 if portfolio is None or not portfolio: 3755 portfolio = self.Overview(show=False) 3756 3757 if self._ticker: 3758 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3759 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3760 3761 for instrument in portfolio["stat"]["orders"]: 3762 if instrument["ticker"] == self._ticker: 3763 result.append(instrument["orderID"]) 3764 3765 if result: 3766 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3767 3768 elif self._figi: 3769 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3770 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3771 3772 for instrument in portfolio["stat"]["orders"]: 3773 if instrument["figi"] == self._figi: 3774 result.append(instrument["orderID"]) 3775 3776 if result: 3777 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3778 3779 else: 3780 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3781 3782 uLogger.debug(msg) 3783 3784 return result 3785 3786 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3787 """ 3788 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3789 3790 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3791 3792 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3793 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3794 """ 3795 result = False 3796 msg = "Instrument not defined!" 3797 3798 if portfolio is None or not portfolio: 3799 portfolio = self.Overview(show=False) 3800 3801 if self._ticker: 3802 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3803 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3804 3805 for instrument in portfolio["stat"]["stopOrders"]: 3806 if instrument["ticker"] == self._ticker: 3807 result = True 3808 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3809 break 3810 3811 elif self._figi: 3812 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3813 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3814 3815 for instrument in portfolio["stat"]["stopOrders"]: 3816 if instrument["figi"] == self._figi: 3817 result = True 3818 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3819 break 3820 3821 else: 3822 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3823 3824 uLogger.debug(msg) 3825 3826 return result 3827 3828 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3829 """ 3830 Returns list with all `orderID`s of opened stop orders for the instrument. 3831 Instrument must be defined by `ticker` (highly priority) or `figi`. 3832 3833 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3834 3835 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3836 :return: list with `orderID`s of stop orders. 3837 """ 3838 result = [] 3839 msg = "Instrument not defined!" 3840 3841 if portfolio is None or not portfolio: 3842 portfolio = self.Overview(show=False) 3843 3844 if self._ticker: 3845 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3846 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3847 3848 for instrument in portfolio["stat"]["stopOrders"]: 3849 if instrument["ticker"] == self._ticker: 3850 result.append(instrument["orderID"]) 3851 3852 if result: 3853 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3854 3855 elif self._figi: 3856 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3857 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3858 3859 for instrument in portfolio["stat"]["stopOrders"]: 3860 if instrument["figi"] == self._figi: 3861 result.append(instrument["orderID"]) 3862 3863 if result: 3864 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3865 3866 else: 3867 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3868 3869 uLogger.debug(msg) 3870 3871 return result 3872 3873 def RequestLimits(self) -> dict: 3874 """ 3875 Method for obtaining the available funds for withdrawal for current `accountId`. 3876 3877 See also: 3878 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3879 - `OverviewLimits()` method 3880 3881 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3882 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3883 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3884 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3885 """ 3886 if self.accountId is None or not self.accountId: 3887 uLogger.error("Variable `accountId` must be defined for using this method!") 3888 raise Exception("Account ID required") 3889 3890 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3891 3892 self.body = str({"accountId": self.accountId}) 3893 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3894 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3895 3896 if self.moreDebug: 3897 uLogger.debug("Records about available funds for withdrawal successfully received") 3898 3899 return rawLimits 3900 3901 def OverviewLimits(self, show: bool = False) -> dict: 3902 """ 3903 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3904 3905 See also: `RequestLimits()`. 3906 3907 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3908 :return: dict with raw parsed data from server and some calculated statistics about it. 3909 """ 3910 if self.accountId is None or not self.accountId: 3911 uLogger.error("Variable `accountId` must be defined for using this method!") 3912 raise Exception("Account ID required") 3913 3914 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3915 3916 view = { 3917 "rawLimits": rawLimits, 3918 "limits": { # parsed data for every currency: 3919 "money": { # this is an array of portfolio currency positions 3920 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3921 }, 3922 "blocked": { # this is an array of blocked currency 3923 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3924 }, 3925 "blockedGuarantee": { # this is locked money under collateral for futures 3926 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3927 }, 3928 }, 3929 } 3930 3931 # --- Prepare text table with limits in human-readable format: 3932 if show: 3933 info = [ 3934 "# Withdrawal limits\n\n", 3935 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3936 "* **Account ID:** [{}]\n".format(self.accountId), 3937 ] 3938 3939 if view["limits"]["money"]: 3940 info.extend([ 3941 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3942 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3943 ]) 3944 3945 else: 3946 info.append("\nNo withdrawal limits\n") 3947 3948 for curr in view["limits"]["money"].keys(): 3949 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3950 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3951 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3952 3953 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3954 "[{}]".format(curr), 3955 "{:.2f}".format(view["limits"]["money"][curr]), 3956 "{:.2f}".format(availableMoney), 3957 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3958 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3959 ) 3960 3961 if curr == "rub": 3962 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3963 3964 else: 3965 info.append(infoStr) 3966 3967 infoText = "".join(info) 3968 3969 uLogger.info(infoText) 3970 3971 if self.withdrawalLimitsFile: 3972 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3973 fH.write(infoText) 3974 3975 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3976 3977 if self.useHTMLReports: 3978 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3979 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3980 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 3981 3982 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 3983 3984 return view 3985 3986 def RequestAccounts(self) -> dict: 3987 """ 3988 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3989 3990 See also: 3991 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3992 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3993 - `OverviewUserInfo()` method 3994 3995 :return: dict with raw data from server that contains accounts info. Example of dict: 3996 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3997 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3998 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3999 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4000 """ 4001 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4002 4003 self.body = str({}) 4004 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4005 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4006 4007 if self.moreDebug: 4008 uLogger.debug("Records about available accounts successfully received") 4009 4010 return rawAccounts 4011 4012 def RequestUserInfo(self) -> dict: 4013 """ 4014 Method for requesting common user's information. 4015 4016 See also: 4017 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4018 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4019 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4020 - `OverviewUserInfo()` method 4021 4022 :return: dict with raw data from server that contains user's information. Example of dict: 4023 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4024 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4025 """ 4026 uLogger.debug("Requesting common user's information. Wait, please...") 4027 4028 self.body = str({}) 4029 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4030 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4031 4032 if self.moreDebug: 4033 uLogger.debug("Records about current user successfully received") 4034 4035 return rawUserInfo 4036 4037 def RequestMarginStatus(self, accountId: str = None) -> dict: 4038 """ 4039 Method for requesting margin calculation for defined account ID. 4040 4041 See also: 4042 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4043 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4044 - `OverviewUserInfo()` method 4045 4046 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4047 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4048 Example of responses: 4049 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4050 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4051 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4052 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4053 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4054 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4055 """ 4056 if accountId is None or not accountId: 4057 if self.accountId is None or not self.accountId: 4058 uLogger.error("Variable `accountId` must be defined for using this method!") 4059 raise Exception("Account ID required") 4060 4061 else: 4062 accountId = self.accountId # use `self.accountId` (main ID) by default 4063 4064 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4065 4066 self.body = str({"accountId": accountId}) 4067 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4068 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4069 4070 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4071 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4072 rawMargin = {} 4073 4074 else: 4075 if self.moreDebug: 4076 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4077 4078 return rawMargin 4079 4080 def RequestTariffLimits(self) -> dict: 4081 """ 4082 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4083 4084 See also: 4085 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4086 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4087 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4088 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4089 - `OverviewUserInfo()` method 4090 4091 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4092 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4093 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4094 """ 4095 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4096 4097 self.body = str({}) 4098 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4099 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4100 4101 if self.moreDebug: 4102 uLogger.debug("Records with limits of current tariff successfully received") 4103 4104 return rawTariffLimits 4105 4106 def RequestBondCoupons(self, iJSON: dict) -> dict: 4107 """ 4108 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4109 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4110 All dates are in UTC timezone. 4111 4112 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4113 Documentation: 4114 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4115 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4116 4117 See also: `ExtendBondsData()`. 4118 4119 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4120 If raw iJSON is not data of bond then server returns an error [400] with message: 4121 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4122 :return: dictionary with bond payment calendar. Response example 4123 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4124 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4125 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4126 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4127 """ 4128 if iJSON["figi"] is None or not iJSON["figi"]: 4129 uLogger.error("FIGI must be defined for using this method!") 4130 raise Exception("FIGI required") 4131 4132 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4133 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4134 4135 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4136 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4137 self._figi, 4138 startDate, 4139 endDate, 4140 )) 4141 4142 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4143 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4144 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4145 4146 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4147 uLogger.warning("Instrument type is not bond!") 4148 4149 else: 4150 if self.moreDebug: 4151 uLogger.debug("Records about bond payment calendar successfully received") 4152 4153 return calendar 4154 4155 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4156 """ 4157 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4158 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4159 coupon yields, current yields and some statistics etc. 4160 4161 WARNING! This is too long operation if a lot of bonds requested from broker server. 4162 4163 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4164 4165 :param instruments: list of strings with tickers or FIGIs. 4166 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4167 for further used by data scientists or stock analytics. 4168 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4169 In XLSX-file and Pandas DataFrame fields mean: 4170 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4171 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4172 """ 4173 if instruments is None or not instruments: 4174 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4175 raise Exception("Ticker or FIGI required") 4176 4177 if isinstance(instruments, str): 4178 instruments = [instruments] 4179 4180 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4181 4182 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4183 4184 iCount = len(uniqueInstruments) 4185 tooLong = iCount >= 20 4186 if tooLong: 4187 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4188 4189 bonds = None 4190 for i, self._figi in enumerate(uniqueInstruments): 4191 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4192 4193 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4194 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4195 rawBond = self.SearchByFIGI(requestPrice=True) 4196 4197 # Widen raw data with UTC current time (iData["actualDateTime"]): 4198 actualDate = datetime.now(tzutc()) 4199 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4200 4201 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4202 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4203 4204 # Replace some values with human-readable: 4205 iData["nominalCurrency"] = iData["nominal"]["currency"] 4206 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4207 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4208 iData["aciCurrency"] = iData["aciValue"]["currency"] 4209 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4210 iData["issueSize"] = int(iData["issueSize"]) 4211 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4212 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4213 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4214 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4215 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4216 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4217 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4218 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4219 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4220 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4221 4222 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4223 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4224 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4225 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4226 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4227 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4228 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4229 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4230 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4231 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4232 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4233 4234 # Widen raw data with calendar data from `rawCalendar` values: 4235 calendarData = [] 4236 if "events" in iData["rawCalendar"].keys(): 4237 for item in iData["rawCalendar"]["events"]: 4238 calendarData.append({ 4239 "couponDate": item["couponDate"], 4240 "couponNumber": int(item["couponNumber"]), 4241 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4242 "payCurrency": item["payOneBond"]["currency"], 4243 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4244 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4245 "couponStartDate": item["couponStartDate"], 4246 "couponEndDate": item["couponEndDate"], 4247 "couponPeriod": item["couponPeriod"], 4248 }) 4249 4250 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4251 if "maturityDate" not in iData.keys(): 4252 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4253 4254 # Widen raw data with Coupon Rate. 4255 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4256 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4257 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4258 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4259 4260 # Widen raw data with Yield to Maturity (YTM) on current date. 4261 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4262 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4263 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4264 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4265 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4266 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4267 4268 iData["calendar"] = calendarData # adds calendar at the end 4269 4270 # Remove not used data: 4271 iData.pop("uid") 4272 iData.pop("positionUid") 4273 iData.pop("currentPrice") 4274 iData.pop("rawCalendar") 4275 4276 colNames = list(iData.keys()) 4277 if bonds is None: 4278 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4279 4280 else: 4281 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4282 4283 else: 4284 uLogger.warning("Instrument is not a bond!") 4285 4286 processed = round(100 * (i + 1) / iCount, 1) 4287 if tooLong and processed % 5 == 0: 4288 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4289 4290 else: 4291 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4292 4293 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4294 4295 # Saving bonds from Pandas DataFrame to XLSX sheet: 4296 if xlsx and self.bondsXLSXFile: 4297 with pd.ExcelWriter( 4298 path=self.bondsXLSXFile, 4299 date_format=TKS_DATE_FORMAT, 4300 datetime_format=TKS_DATE_TIME_FORMAT, 4301 mode="w", 4302 ) as writer: 4303 bonds.to_excel( 4304 writer, 4305 sheet_name="Extended bonds data", 4306 index=True, 4307 encoding="UTF-8", 4308 freeze_panes=(1, 1), 4309 ) # saving as XLSX-file with freeze first row and column as headers 4310 4311 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4312 4313 return bonds 4314 4315 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4316 """ 4317 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4318 4319 WARNING! This is too long operation if a lot of bonds requested from broker server. 4320 4321 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4322 4323 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4324 extended information about bonds: main info, current prices, bond payment calendar, 4325 coupon yields, current yields and some statistics etc. 4326 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4327 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4328 for further used by data scientists or stock analytics. 4329 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4330 """ 4331 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4332 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4333 4334 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4335 4336 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4337 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4338 calendar = None 4339 for bond in extBonds.iterrows(): 4340 for item in bond[1]["calendar"]: 4341 cData = { 4342 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4343 "couponDate": item["couponDate"], 4344 "figi": bond[1]["figi"], 4345 "ticker": bond[1]["ticker"], 4346 "name": bond[1]["name"], 4347 "couponNumber": item["couponNumber"], 4348 "payOneBond": item["payOneBond"], 4349 "payCurrency": item["payCurrency"], 4350 "couponType": item["couponType"], 4351 "couponPeriod": item["couponPeriod"], 4352 "fixDate": item["fixDate"], 4353 "couponStartDate": item["couponStartDate"], 4354 "couponEndDate": item["couponEndDate"], 4355 } 4356 4357 if calendar is None: 4358 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4359 4360 else: 4361 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4362 4363 if calendar is not None: 4364 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4365 4366 # Saving calendar from Pandas DataFrame to XLSX sheet: 4367 if xlsx: 4368 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4369 4370 with pd.ExcelWriter( 4371 path=xlsxCalendarFile, 4372 date_format=TKS_DATE_FORMAT, 4373 datetime_format=TKS_DATE_TIME_FORMAT, 4374 mode="w", 4375 ) as writer: 4376 humanReadable = calendar.copy(deep=True) 4377 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4378 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4379 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4380 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4381 humanReadable.columns = colNames # human-readable column names 4382 4383 humanReadable.to_excel( 4384 writer, 4385 sheet_name="Bond payments calendar", 4386 index=False, 4387 encoding="UTF-8", 4388 freeze_panes=(1, 2), 4389 ) # saving as XLSX-file with freeze first row and column as headers 4390 4391 del humanReadable # release df in memory 4392 4393 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4394 4395 return calendar 4396 4397 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4398 """ 4399 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4400 Also, creates Markdown file with calendar data, `calendar.md` by default. 4401 4402 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4403 4404 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4405 extended information about bonds: main info, current prices, bond payment calendar, 4406 coupon yields, current yields and some statistics etc. 4407 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4408 :param show: if `True` then also printing bonds payment calendar to the console, 4409 otherwise save to file `calendarFile` only. `False` by default. 4410 :return: multilines text in Markdown format with bonds payment calendar as a table. 4411 """ 4412 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4413 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4414 4415 infoText = "# Bond payments calendar\n\n" 4416 4417 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4418 4419 if not (calendar is None or calendar.empty): 4420 splitLine = "| | | | | | | | | |\n" 4421 4422 info = [ 4423 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4424 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4425 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4426 ] 4427 4428 newMonth = False 4429 notOneBond = calendar["figi"].nunique() > 1 4430 for i, bond in enumerate(calendar.iterrows()): 4431 if newMonth and notOneBond: 4432 info.append(splitLine) 4433 4434 info.append( 4435 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4436 " √" if bond[1]["paid"] else " —", 4437 bond[1]["couponDate"].split("T")[0], 4438 bond[1]["figi"], 4439 bond[1]["ticker"], 4440 bond[1]["couponNumber"], 4441 "{} {}".format( 4442 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4443 bond[1]["payCurrency"], 4444 ), 4445 bond[1]["couponType"], 4446 bond[1]["couponPeriod"], 4447 bond[1]["fixDate"].split("T")[0], 4448 ) 4449 ) 4450 4451 if i < len(calendar.values) - 1: 4452 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4453 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4454 newMonth = False if curDate.month == nextDate.month else True 4455 4456 else: 4457 newMonth = False 4458 4459 infoText += "".join(info) 4460 4461 if show: 4462 uLogger.info("{}".format(infoText)) 4463 4464 if self.calendarFile is not None: 4465 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4466 fH.write(infoText) 4467 4468 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4469 4470 if self.useHTMLReports: 4471 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4472 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4473 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4474 4475 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4476 4477 else: 4478 infoText += "No data\n" 4479 4480 return infoText 4481 4482 def OverviewAccounts(self, show: bool = False) -> dict: 4483 """ 4484 Method for parsing and show simple table with all available user accounts. 4485 4486 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4487 4488 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4489 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4490 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4491 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4492 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4493 "closed": "—", "access": "Full access" }, ...}}` 4494 """ 4495 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4496 4497 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4498 accounts = { 4499 item["id"]: { 4500 "type": TKS_ACCOUNT_TYPES[item["type"]], 4501 "name": item["name"], 4502 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4503 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4504 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4505 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4506 } for item in rawAccounts["accounts"] 4507 } 4508 4509 # Raw and parsed data with some fields replaced in "stat" section: 4510 view = { 4511 "rawAccounts": rawAccounts, 4512 "stat": accounts, 4513 } 4514 4515 # --- Prepare simple text table with only accounts data in human-readable format: 4516 if show: 4517 info = [ 4518 "# User accounts\n\n", 4519 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4520 "| Account ID | Type | Status | Name |\n", 4521 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4522 ] 4523 4524 for account in view["stat"].keys(): 4525 info.extend([ 4526 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4527 account, 4528 view["stat"][account]["type"], 4529 view["stat"][account]["status"], 4530 view["stat"][account]["name"], 4531 ) 4532 ]) 4533 4534 infoText = "".join(info) 4535 4536 uLogger.info(infoText) 4537 4538 if self.userAccountsFile: 4539 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4540 fH.write(infoText) 4541 4542 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4543 4544 if self.useHTMLReports: 4545 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4546 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4547 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4548 4549 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4550 4551 return view 4552 4553 def OverviewUserInfo(self, show: bool = False) -> dict: 4554 """ 4555 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4556 4557 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4558 4559 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4560 :return: dict with raw parsed data from server and some calculated statistics about it. 4561 """ 4562 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4563 tmpTicker = self._ticker 4564 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4565 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4566 self._ticker = tmpTicker 4567 4568 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4569 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4570 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4571 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4572 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4573 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4574 4575 # This is dict with parsed common user data: 4576 userInfo = { 4577 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4578 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4579 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4580 "tariff": rawUserInfo["tariff"], 4581 } 4582 4583 # This is an array of dict with parsed margin statuses for every account IDs: 4584 margins = {} 4585 for accountId in accounts.keys(): 4586 if rawMargins[accountId]: 4587 margins[accountId] = { 4588 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4589 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4590 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4591 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4592 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4593 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4594 "missing": missing["volume"], 4595 } 4596 4597 else: 4598 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4599 4600 unary = {} # unary-connection limits 4601 for item in rawTariffLimits["unaryLimits"]: 4602 if item["limitPerMinute"] in unary.keys(): 4603 unary[item["limitPerMinute"]].extend(item["methods"]) 4604 4605 else: 4606 unary[item["limitPerMinute"]] = item["methods"] 4607 4608 stream = {} # stream-connection limits 4609 for item in rawTariffLimits["streamLimits"]: 4610 if item["limit"] in stream.keys(): 4611 stream[item["limit"]].extend(item["streams"]) 4612 4613 else: 4614 stream[item["limit"]] = item["streams"] 4615 4616 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4617 limits = { 4618 "unary": unary, 4619 "stream": stream, 4620 } 4621 4622 # Raw and parsed data as an output result: 4623 view = { 4624 "rawUserInfo": rawUserInfo, 4625 "rawAccounts": rawAccounts, 4626 "rawMargins": rawMargins, 4627 "rawTariffLimits": rawTariffLimits, 4628 "stat": { 4629 "overview": overview, 4630 "userInfo": userInfo, 4631 "accounts": accounts, 4632 "margins": margins, 4633 "limits": limits, 4634 }, 4635 } 4636 4637 # --- Prepare text table with user information in human-readable format: 4638 if show: 4639 info = [ 4640 "# Full user information\n\n", 4641 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4642 "## Common information\n\n", 4643 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4644 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4645 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4646 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4647 "\n## User accounts\n\n", 4648 ] 4649 4650 for account in view["stat"]["accounts"].keys(): 4651 info.extend([ 4652 "### ID: [{}]\n\n".format(account), 4653 "| Parameters | Values |\n", 4654 "|----------------------|--------------------------------------------------------------|\n", 4655 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4656 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4657 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4658 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4659 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4660 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4661 ]) 4662 4663 if margins[account]: 4664 info.extend([ 4665 "| Margin status: | Enabled |\n", 4666 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4667 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4668 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4669 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4670 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4671 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4672 ]) 4673 4674 else: 4675 info.append("| Margin status: | Disabled |\n\n") 4676 4677 info.extend([ 4678 "\n## Current user tariff limits\n", 4679 "\n### See also\n", 4680 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4681 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4682 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4683 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4684 "\n### Unary limits\n", 4685 ]) 4686 4687 if unary: 4688 for key, values in sorted(unary.items()): 4689 info.append("\n* Max requests per minute: {}\n".format(key)) 4690 4691 for value in values: 4692 info.append(" - {}\n".format(value)) 4693 4694 else: 4695 info.append("\nNot available\n") 4696 4697 info.append("\n### Stream limits\n") 4698 4699 if stream: 4700 for key, values in sorted(stream.items()): 4701 info.append("\n* Max stream connections: {}\n".format(key)) 4702 4703 for value in values: 4704 info.append(" - {}\n".format(value)) 4705 4706 else: 4707 info.append("\nNot available\n") 4708 4709 infoText = "".join(info) 4710 4711 uLogger.info(infoText) 4712 4713 if self.userInfoFile: 4714 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4715 fH.write(infoText) 4716 4717 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4718 4719 if self.useHTMLReports: 4720 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4721 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4722 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4723 4724 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4725 4726 return view 4727 4728 4729class Args: 4730 """ 4731 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4732 """ 4733 def __init__(self, **kwargs): 4734 self.__dict__.update(kwargs) 4735 4736 def __getattr__(self, item): 4737 return None 4738 4739 4740def ParseArgs(): 4741 """This function get and parse command line keys.""" 4742 parser = ArgumentParser() # command-line string parser 4743 4744 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4745 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4746 4747 # --- options: 4748 4749 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4750 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4751 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4752 4753 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4754 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4755 4756 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4757 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4758 4759 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4760 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4761 4762 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4763 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4764 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4765 4766 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4767 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4768 4769 # --- commands: 4770 4771 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4772 4773 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4774 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4775 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4776 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4777 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4778 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4779 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4780 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4781 4782 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4783 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4784 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4785 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4786 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4787 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4788 4789 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4790 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4791 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4792 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4793 4794 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4795 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4796 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4797 4798 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4799 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4800 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4801 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4802 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4803 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4804 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4805 4806 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4807 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4808 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4809 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4810 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4811 4812 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4813 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4814 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4815 4816 cmdArgs = parser.parse_args() 4817 return cmdArgs 4818 4819 4820def Main(**kwargs): 4821 """ 4822 Main function for work with TKSBrokerAPI in the console. 4823 4824 See examples: 4825 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4826 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4827 """ 4828 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4829 4830 if args.debug_level: 4831 uLogger.level = 10 # always debug level by default 4832 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4833 4834 exitCode = 0 4835 start = datetime.now(tzutc()) 4836 uLogger.debug("=-" * 50) 4837 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4838 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4839 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4840 )) 4841 4842 # trying to calculate full current version: 4843 buildVersion = __version__ 4844 try: 4845 v = version("tksbrokerapi") 4846 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4847 4848 except Exception: 4849 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4850 4851 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4852 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4853 4854 try: 4855 if args.version: 4856 print("TKSBrokerAPI {}".format(buildVersion)) 4857 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4858 4859 else: 4860 # Init class for trading with Tinkoff Broker: 4861 trader = TinkoffBrokerServer( 4862 token=args.token, 4863 accountId=args.account_id, 4864 useCache=not args.no_cache, 4865 ) 4866 4867 # --- set some options: 4868 4869 if args.more: 4870 trader.moreDebug = True 4871 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4872 4873 if args.html: 4874 trader.useHTMLReports = True 4875 4876 if args.ticker: 4877 ticker = str(args.ticker).upper() # Tickers may be upper case only 4878 4879 if ticker in trader.aliasesKeys: 4880 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4881 4882 else: 4883 trader.ticker = ticker 4884 4885 if args.figi: 4886 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4887 4888 if args.depth is not None: 4889 trader.depth = args.depth 4890 4891 # --- do one command: 4892 4893 if args.list: 4894 if args.output is not None: 4895 trader.instrumentsFile = args.output 4896 4897 trader.ShowInstrumentsInfo(show=True) 4898 4899 elif args.list_xlsx: 4900 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4901 4902 elif args.bonds_xlsx is not None: 4903 if args.output is not None: 4904 trader.bondsXLSXFile = args.output 4905 4906 if len(args.bonds_xlsx) == 0: 4907 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4908 4909 else: 4910 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4911 4912 elif args.search: 4913 if args.output is not None: 4914 trader.searchResultsFile = args.output 4915 4916 trader.SearchInstruments(pattern=args.search[0], show=True) 4917 4918 elif args.info: 4919 if not (args.ticker or args.figi): 4920 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4921 raise Exception("Ticker or FIGI required") 4922 4923 if args.output is not None: 4924 trader.infoFile = args.output 4925 4926 if args.ticker: 4927 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4928 4929 else: 4930 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4931 4932 elif args.calendar is not None: 4933 if args.output is not None: 4934 trader.calendarFile = args.output 4935 4936 if len(args.calendar) == 0: 4937 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4938 4939 else: 4940 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4941 4942 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4943 4944 elif args.price: 4945 if not (args.ticker or args.figi): 4946 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4947 raise Exception("Ticker or FIGI required") 4948 4949 trader.GetCurrentPrices(show=True) 4950 4951 elif args.prices is not None: 4952 if args.output is not None: 4953 trader.pricesFile = args.output 4954 4955 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4956 4957 elif args.overview: 4958 if args.output is not None: 4959 trader.overviewFile = args.output 4960 4961 trader.Overview(show=True, details="full") 4962 4963 elif args.overview_digest: 4964 if args.output is not None: 4965 trader.overviewDigestFile = args.output 4966 4967 trader.Overview(show=True, details="digest") 4968 4969 elif args.overview_positions: 4970 if args.output is not None: 4971 trader.overviewPositionsFile = args.output 4972 4973 trader.Overview(show=True, details="positions") 4974 4975 elif args.overview_orders: 4976 if args.output is not None: 4977 trader.overviewOrdersFile = args.output 4978 4979 trader.Overview(show=True, details="orders") 4980 4981 elif args.overview_analytics: 4982 if args.output is not None: 4983 trader.overviewAnalyticsFile = args.output 4984 4985 trader.Overview(show=True, details="analytics") 4986 4987 elif args.overview_calendar: 4988 if args.output is not None: 4989 trader.overviewAnalyticsFile = args.output 4990 4991 trader.Overview(show=True, details="calendar") 4992 4993 elif args.deals is not None: 4994 if args.output is not None: 4995 trader.reportFile = args.output 4996 4997 if 0 <= len(args.deals) < 3: 4998 trader.Deals( 4999 start=args.deals[0] if len(args.deals) >= 1 else None, 5000 end=args.deals[1] if len(args.deals) == 2 else None, 5001 show=True, # Always show deals report in console 5002 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 5003 ) 5004 5005 else: 5006 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5007 raise Exception("Incorrect value") 5008 5009 elif args.history is not None: 5010 if args.output is not None: 5011 trader.historyFile = args.output 5012 5013 if 0 <= len(args.history) < 3: 5014 dataReceived = trader.History( 5015 start=args.history[0] if len(args.history) >= 1 else None, 5016 end=args.history[1] if len(args.history) == 2 else None, 5017 interval="hour" if args.interval is None or not args.interval else args.interval, 5018 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5019 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5020 show=True, # shows all downloaded candles in console 5021 ) 5022 5023 if args.render_chart is not None and dataReceived is not None: 5024 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5025 5026 trader.ShowHistoryChart( 5027 candles=dataReceived, 5028 interact=iChart, 5029 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5030 ) 5031 5032 else: 5033 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5034 raise Exception("Incorrect value") 5035 5036 elif args.load_history is not None: 5037 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5038 5039 if args.render_chart is not None and histData is not None: 5040 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5041 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5042 5043 trader.ShowHistoryChart( 5044 candles=histData, 5045 interact=iChart, 5046 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5047 ) 5048 5049 elif args.trade is not None: 5050 if 1 <= len(args.trade) <= 5: 5051 trader.Trade( 5052 operation=args.trade[0], 5053 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5054 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5055 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5056 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5057 ) 5058 5059 else: 5060 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5061 5062 elif args.buy is not None: 5063 if 0 <= len(args.buy) <= 4: 5064 trader.Buy( 5065 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5066 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5067 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5068 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5069 ) 5070 5071 else: 5072 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5073 5074 elif args.sell is not None: 5075 if 0 <= len(args.sell) <= 4: 5076 trader.Sell( 5077 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5078 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5079 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5080 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5081 ) 5082 5083 else: 5084 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5085 5086 elif args.order: 5087 if 4 <= len(args.order) <= 7: 5088 trader.Order( 5089 operation=args.order[0], 5090 orderType=args.order[1], 5091 lots=int(args.order[2]), 5092 targetPrice=float(args.order[3]), 5093 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5094 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5095 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5096 ) 5097 5098 else: 5099 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5100 5101 elif args.buy_limit: 5102 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5103 5104 elif args.sell_limit: 5105 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5106 5107 elif args.buy_stop: 5108 if 2 <= len(args.buy_stop) <= 7: 5109 trader.BuyStop( 5110 lots=int(args.buy_stop[0]), 5111 targetPrice=float(args.buy_stop[1]), 5112 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5113 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5114 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5115 ) 5116 5117 else: 5118 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5119 5120 elif args.sell_stop: 5121 if 2 <= len(args.sell_stop) <= 7: 5122 trader.SellStop( 5123 lots=int(args.sell_stop[0]), 5124 targetPrice=float(args.sell_stop[1]), 5125 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5126 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5127 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5128 ) 5129 5130 else: 5131 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5132 5133 # elif args.buy_order_grid is not None: 5134 # # update order grid work with api v2 5135 # if len(args.buy_order_grid) == 2: 5136 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5137 # 5138 # for order in orderParams: 5139 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5140 # 5141 # else: 5142 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5143 # 5144 # elif args.sell_order_grid is not None: 5145 # # update order grid work with api v2 5146 # if len(args.sell_order_grid) >= 2: 5147 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5148 # 5149 # for order in orderParams: 5150 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5151 # 5152 # else: 5153 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5154 5155 elif args.close_order is not None: 5156 trader.CloseOrders(args.close_order) # close only one order 5157 5158 elif args.close_orders is not None: 5159 trader.CloseOrders(args.close_orders) # close list of orders 5160 5161 elif args.close_trade: 5162 if not (args.ticker or args.figi): 5163 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5164 raise Exception("Ticker or FIGI required") 5165 5166 if args.ticker: 5167 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5168 5169 else: 5170 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5171 5172 elif args.close_trades is not None: 5173 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5174 5175 elif args.close_all is not None: 5176 if args.ticker: 5177 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5178 5179 elif args.figi: 5180 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5181 5182 else: 5183 trader.CloseAll(*args.close_all) 5184 5185 elif args.limits: 5186 if args.output is not None: 5187 trader.withdrawalLimitsFile = args.output 5188 5189 trader.OverviewLimits(show=True) 5190 5191 elif args.user_info: 5192 if args.output is not None: 5193 trader.userInfoFile = args.output 5194 5195 trader.OverviewUserInfo(show=True) 5196 5197 elif args.account: 5198 if args.output is not None: 5199 trader.userAccountsFile = args.output 5200 5201 trader.OverviewAccounts(show=True) 5202 5203 else: 5204 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5205 raise Exception("There is no command to execute") 5206 5207 except Exception: 5208 trace = tb.format_exc() 5209 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5210 if e in trace: 5211 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5212 break 5213 5214 uLogger.debug(trace) 5215 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5216 exitCode = 255 # an error occurred, must be open a ticket for this issue 5217 5218 finally: 5219 finish = datetime.now(tzutc()) 5220 5221 if exitCode == 0: 5222 if args.more: 5223 uLogger.debug("All operations were finished success (summary code is 0).") 5224 5225 else: 5226 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5227 os.path.abspath(uLog.defaultLogFile), exitCode, 5228 )) 5229 5230 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5231 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5232 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5233 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5234 )) 5235 uLogger.debug("=-" * 50) 5236 5237 if not kwargs: 5238 sys.exit(exitCode) 5239 5240 else: 5241 return exitCode 5242 5243 5244if __name__ == "__main__": 5245 Main()
78class TinkoffBrokerServer: 79 """ 80 This class implements methods to work with Tinkoff broker server. 81 82 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 83 84 About `token`: https://tinkoff.github.io/investAPI/token/ 85 """ 86 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 87 """ 88 Main class init. 89 90 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 91 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 92 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 93 :param useCache: use default cache file with raw data to use instead of `iList`. 94 True by default. Cache is auto-update if new day has come. 95 If you don't want to use cache and always updates raw data then set `useCache=False`. 96 :param defaultCache: path to default cache file. `dump.json` by default. 97 """ 98 if token is None or not token: 99 try: 100 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 101 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 102 103 except KeyError: 104 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 105 raise Exception("Token required") 106 107 else: 108 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 109 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 110 111 if accountId is None or not accountId: 112 try: 113 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 114 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 115 116 except KeyError: 117 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 118 119 else: 120 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 121 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 122 123 self.version = __version__ # duplicate here used TKSBrokerAPI main version 124 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 125 126 Latest version: https://pypi.org/project/tksbrokerapi/ 127 """ 128 129 self.__lock = Lock() # initialize multiprocessing mutex lock 130 131 self.aliases = TKS_TICKER_ALIASES 132 """Some aliases instead official tickers. 133 134 See also: `TKSEnums.TKS_TICKER_ALIASES` 135 """ 136 137 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 138 139 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 140 141 self._ticker = "" 142 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 143 144 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 145 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 146 147 See also: `SearchByTicker()`, `SearchInstruments()`. 148 """ 149 150 self._figi = "" 151 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 152 153 See also: `SearchByFIGI()`, `SearchInstruments()`. 154 """ 155 156 self.depth = 1 157 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 158 159 See also: `GetCurrentPrices()`. 160 """ 161 162 self.server = r"https://invest-public-api.tinkoff.ru/rest" 163 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 164 165 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 166 """ 167 168 uLogger.debug("Broker API server: {}".format(self.server)) 169 170 self.timeout = 15 171 """Server operations timeout in seconds. Default: `15`. 172 173 See also: `SendAPIRequest()`. 174 """ 175 176 self.headers = { 177 "Content-Type": "application/json", 178 "accept": "application/json", 179 "Authorization": "Bearer {}".format(self.token), 180 "x-app-name": "Tim55667757.TKSBrokerAPI", 181 } 182 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 183 184 See also: `SendAPIRequest()`. 185 """ 186 187 self.body = None 188 """Request body which send to broker server. Default: `None`. 189 190 See also: `SendAPIRequest()`. 191 """ 192 193 self.moreDebug = False 194 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 195 196 self.useHTMLReports = False 197 """ 198 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 199 200 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 201 """ 202 203 self.historyFile = None 204 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 205 206 See also: `History()`. 207 """ 208 209 self.htmlHistoryFile = "index.html" 210 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 211 212 See also: `ShowHistoryChart()`. 213 """ 214 215 self.instrumentsFile = "instruments.md" 216 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 217 218 See also: `ShowInstrumentsInfo()`. 219 """ 220 221 self.searchResultsFile = "search-results.md" 222 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 223 224 See also: `SearchInstruments()`. 225 """ 226 227 self.pricesFile = "prices.md" 228 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 229 230 See also: `GetListOfPrices()`. 231 """ 232 233 self.infoFile = "info.md" 234 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 235 236 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 237 """ 238 239 self.bondsXLSXFile = "ext-bonds.xlsx" 240 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 241 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 242 243 See also: `ExtendBondsData()`. 244 """ 245 246 self.calendarFile = "calendar.md" 247 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 248 249 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 250 251 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 252 """ 253 254 self.overviewFile = "overview.md" 255 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 256 257 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 258 """ 259 260 self.overviewDigestFile = "overview-digest.md" 261 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 262 263 See also: `Overview()` with parameter `details="digest"`. 264 """ 265 266 self.overviewPositionsFile = "overview-positions.md" 267 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 268 269 See also: `Overview()` with parameter `details="positions"`. 270 """ 271 272 self.overviewOrdersFile = "overview-orders.md" 273 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 274 275 See also: `Overview()` with parameter `details="orders"`. 276 """ 277 278 self.overviewAnalyticsFile = "overview-analytics.md" 279 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 280 281 See also: `Overview()` with parameter `details="analytics"`. 282 """ 283 284 self.overviewBondsCalendarFile = "overview-calendar.md" 285 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 286 287 See also: `Overview()` with parameter `details="calendar"`. 288 """ 289 290 self.reportFile = "deals.md" 291 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 292 293 See also: `Deals()`. 294 """ 295 296 self.withdrawalLimitsFile = "limits.md" 297 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 298 299 See also: `OverviewLimits()` and `RequestLimits()`. 300 """ 301 302 self.userInfoFile = "user-info.md" 303 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 304 305 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 306 """ 307 308 self.userAccountsFile = "accounts.md" 309 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 310 311 See also: `OverviewAccounts()`, `RequestAccounts()`. 312 """ 313 314 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 315 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 316 317 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 318 319 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 320 """ 321 322 self.iList = None # init iList for raw instruments data 323 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 324 325 See also: `Listing()`, `DumpInstruments()`. 326 """ 327 328 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 329 if useCache: 330 if os.path.exists(self.iListDumpFile): 331 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 332 curTime = datetime.now(tzutc()) 333 334 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 335 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 336 337 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 338 339 else: 340 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 341 342 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 343 os.path.abspath(self.iListDumpFile), 344 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 345 )) 346 347 else: 348 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 349 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 350 351 else: 352 self.iList = self.Listing() # request new raw instruments data from broker server 353 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 354 355 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 356 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 357 358 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 359 """ 360 361 @property 362 def ticker(self) -> str: 363 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 364 365 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 366 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 367 368 See also: `SearchByTicker()`, `SearchInstruments()`. 369 """ 370 return self._ticker 371 372 @ticker.setter 373 def ticker(self, value): 374 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 375 376 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 377 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 378 379 See also: `SearchByTicker()`, `SearchInstruments()`. 380 """ 381 self._ticker = str(value).upper() # Tickers may be upper case only 382 383 @property 384 def figi(self) -> str: 385 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 386 387 See also: `SearchByFIGI()`, `SearchInstruments()`. 388 """ 389 return self._figi 390 391 @figi.setter 392 def figi(self, value): 393 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 394 395 See also: `SearchByFIGI()`, `SearchInstruments()`. 396 """ 397 self._figi = str(value).upper() # FIGI may be upper case only 398 399 def _ParseJSON(self, rawData="{}") -> dict: 400 """ 401 Parse JSON from response string. 402 403 :param rawData: this is a string with JSON-formatted text. 404 :return: JSON (dictionary), parsed from server response string. 405 """ 406 responseJSON = json.loads(rawData) if rawData else {} 407 408 if self.moreDebug: 409 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 410 411 return responseJSON 412 413 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 414 """ 415 Send GET or POST request to broker server and receive JSON object. 416 417 self.header: must be defining with dictionary of headers. 418 self.body: if define then used as request body. None by default. 419 self.timeout: global request timeout, 15 seconds by default. 420 :param url: url with REST request. 421 :param reqType: send "GET" or "POST" request. "GET" by default. 422 :param retry: how many times retry after first request if an 5xx server errors occurred. 423 :param pause: sleep time in seconds between retries. 424 :return: response JSON (dictionary) from broker. 425 """ 426 if reqType.upper() not in ("GET", "POST"): 427 uLogger.error("You can define request type: `GET` or `POST`!") 428 raise Exception("Incorrect value") 429 430 if self.moreDebug: 431 uLogger.debug("Request parameters:") 432 uLogger.debug(" - REST API URL: {}".format(url)) 433 uLogger.debug(" - request type: {}".format(reqType)) 434 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 435 uLogger.debug(" - body:\n{}".format(self.body)) 436 437 # fast hack to avoid all operations with some tickers/FIGI 438 responseJSON = {} 439 oK = True 440 for item in self.exclude: 441 if item in url: 442 if self.moreDebug: 443 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 444 445 oK = False 446 break 447 448 if oK: 449 with self.__lock: # acquire the mutex lock 450 counter = 0 451 response = None 452 errMsg = "" 453 454 while not response and counter <= retry: 455 if reqType == "GET": 456 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 457 458 if reqType == "POST": 459 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 460 461 if self.moreDebug: 462 uLogger.debug("Response:") 463 uLogger.debug(" - status code: {}".format(response.status_code)) 464 uLogger.debug(" - reason: {}".format(response.reason)) 465 uLogger.debug(" - body length: {}".format(len(response.text))) 466 uLogger.debug(" - headers:\n{}".format(response.headers)) 467 468 # Server returns some headers: 469 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 470 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 471 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 472 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 473 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 474 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 475 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 476 sleep(rateLimitWait) 477 478 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 479 if 400 <= response.status_code < 500: 480 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 481 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 482 483 if "code" in response.text and "message" in response.text: 484 msgDict = self._ParseJSON(rawData=response.text) 485 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 486 487 counter = retry + 1 # do not retry for 4xx errors 488 489 if 500 <= response.status_code < 600: 490 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 491 uLogger.debug(" - not oK, {}".format(errMsg)) 492 493 if "code" in response.text and "message" in response.text: 494 errMsgDict = self._ParseJSON(rawData=response.text) 495 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 496 497 counter += 1 498 499 if counter <= retry: 500 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 501 sleep(pause) 502 503 responseJSON = self._ParseJSON(rawData=response.text) 504 505 if errMsg: 506 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 507 uLogger.error(" - not oK, {}".format(errMsg)) 508 509 return responseJSON 510 511 def _IUpdater(self, iType: str) -> tuple: 512 """ 513 Request instrument by type from server. See available API methods for instruments: 514 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 515 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 516 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 517 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 518 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 519 520 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 521 :return: tuple with iType name and list of available instruments of current type for defined user token. 522 """ 523 result = [] 524 525 if iType in TKS_INSTRUMENTS: 526 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 527 528 # all instruments have the same body in API v2 requests: 529 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 530 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 531 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 532 533 return iType, result 534 535 def _IWrapper(self, kwargs): 536 """ 537 Wrapper runs instrument's update method `_IUpdater()`. 538 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 539 """ 540 return self._IUpdater(**kwargs) 541 542 def Listing(self) -> dict: 543 """ 544 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 545 546 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 547 """ 548 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 549 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 550 551 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 552 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 553 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 554 555 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 556 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 557 poolUpdater.close() # close the thread pool 558 poolUpdater.join() # wait a moment until all data returns from threads 559 560 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 561 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 562 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 563 564 # calculate minimum price increment (step) for all instruments and set up instrument's type: 565 for iType in iList.keys(): 566 for ticker in iList[iType]: 567 iList[iType][ticker]["type"] = iType 568 569 if "minPriceIncrement" in iList[iType][ticker].keys(): 570 iList[iType][ticker]["step"] = NanoToFloat( 571 iList[iType][ticker]["minPriceIncrement"]["units"], 572 iList[iType][ticker]["minPriceIncrement"]["nano"], 573 ) 574 575 else: 576 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 577 578 return iList 579 580 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 581 """ 582 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 583 584 See also: `DumpInstruments()`, `Listing()`. 585 586 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 587 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 588 """ 589 if self.iListDumpFile is None or not self.iListDumpFile: 590 uLogger.error("Output name of dump file must be defined!") 591 raise Exception("Filename required") 592 593 if not self.iList or forceUpdate: 594 self.iList = self.Listing() 595 596 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 597 598 # Save as XLSX with separated sheets for every type of instruments: 599 with pd.ExcelWriter( 600 path=xlsxDumpFile, 601 date_format=TKS_DATE_FORMAT, 602 datetime_format=TKS_DATE_TIME_FORMAT, 603 mode="w", 604 ) as writer: 605 for iType in TKS_INSTRUMENTS: 606 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 607 df = df[sorted(df)] # sorted by column names 608 df = df.applymap( 609 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 610 na_action="ignore", 611 ) # converting numbers from nano-type to float in every cell 612 df.to_excel( 613 writer, 614 sheet_name=iType, 615 encoding="UTF-8", 616 freeze_panes=(1, 1), 617 ) # saving as XLSX-file with freeze first row and column as headers 618 619 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 620 621 def DumpInstruments(self, forceUpdate: bool = True) -> str: 622 """ 623 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 624 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 625 626 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 627 628 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 629 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 630 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 631 """ 632 if self.iListDumpFile is None or not self.iListDumpFile: 633 uLogger.error("Output name of dump file must be defined!") 634 raise Exception("Filename required") 635 636 if not self.iList or forceUpdate: 637 self.iList = self.Listing() 638 639 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 640 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 641 fH.write(jsonDump) 642 643 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 644 645 return jsonDump 646 647 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 648 """ 649 Show information about one instrument defined by json data and prints it in Markdown format. 650 651 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 652 653 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 654 :param show: if `True` then also printing information about instrument and its current price. 655 :return: multilines text in Markdown format with information about one instrument. 656 """ 657 splitLine = "| | |\n" 658 infoText = "" 659 660 if iJSON is not None and iJSON and isinstance(iJSON, dict): 661 info = [ 662 "# Main information\n\n", 663 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 664 "| Parameters | Values |\n", 665 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 666 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 667 "| Full name: | {:<54} |\n".format(iJSON["name"]), 668 ] 669 670 if "sector" in iJSON.keys() and iJSON["sector"]: 671 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 672 673 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 674 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 675 676 info.extend([ 677 splitLine, 678 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 679 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 680 ]) 681 682 if "isin" in iJSON.keys() and iJSON["isin"]: 683 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 684 685 if "classCode" in iJSON.keys(): 686 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 687 688 info.extend([ 689 splitLine, 690 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 691 splitLine, 692 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 693 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 694 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 695 ]) 696 697 if iJSON["figi"]: 698 self._figi = iJSON["figi"] 699 iJSON = iJSON | self.RequestTradingStatus() 700 701 info.extend([ 702 splitLine, 703 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 704 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 705 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 706 ]) 707 708 info.append(splitLine) 709 710 if "type" in iJSON.keys() and iJSON["type"]: 711 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 712 713 if "shareType" in iJSON.keys() and iJSON["shareType"]: 714 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 715 716 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 717 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 718 719 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 720 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 721 722 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 723 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 724 725 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 726 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 727 728 if "focusType" in iJSON.keys() and iJSON["focusType"]: 729 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 730 731 if "assetType" in iJSON.keys() and iJSON["assetType"]: 732 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 733 734 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 735 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 736 737 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 738 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 739 740 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 741 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 742 743 if "currency" in iJSON.keys(): 744 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 745 746 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 747 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 748 749 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 750 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 751 752 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 753 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 754 755 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 756 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 757 758 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 759 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 760 761 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 762 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 763 764 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 765 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 766 767 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 768 info.append("| Perpetual bond: | Yes |\n") 769 770 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 771 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 772 773 iExt = None 774 if iJSON["type"] == "Bonds": 775 info.extend([ 776 splitLine, 777 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 778 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 779 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 780 iJSON["nominal"]["currency"], 781 )), 782 ]) 783 784 if "floatingCouponFlag" in iJSON.keys(): 785 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 786 787 if "amortizationFlag" in iJSON.keys(): 788 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 789 790 info.append(splitLine) 791 792 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 793 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 794 795 if iJSON["figi"]: 796 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 797 798 info.extend([ 799 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 800 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 801 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 802 ]) 803 804 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 805 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 806 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 807 iJSON["aciValue"]["currency"] 808 ))) 809 810 if "currentPrice" in iJSON.keys(): 811 info.append(splitLine) 812 813 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 814 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 815 816 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 817 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 818 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 819 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 820 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 821 822 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 823 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 824 825 info.extend([ 826 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 827 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 828 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 829 )), 830 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 831 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 832 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 833 )), 834 "| Changes between last deal price and last close | {:<54} |\n".format( 835 "{:.2f}%{}".format( 836 iJSON["currentPrice"]["changes"], 837 " ({}{:.2f} {})".format( 838 "+" if bondChangesDelta > 0 else "", 839 bondChangesDelta, 840 aciCurrency 841 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 842 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 843 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 844 currency 845 ), 846 ) 847 ), 848 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 849 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 850 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 851 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 852 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 853 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 854 )), 855 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 856 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 857 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 858 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 859 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 860 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 861 )), 862 ]) 863 864 if "lot" in iJSON.keys(): 865 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 866 867 if "step" in iJSON.keys() and iJSON["step"] != 0: 868 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 869 870 # Add bond payment calendar: 871 if iJSON["type"] == "Bonds": 872 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 873 info.extend(["\n#", strCalendar]) 874 875 infoText += "".join(info) 876 877 if show: 878 uLogger.info("{}".format(infoText)) 879 880 else: 881 uLogger.debug("{}".format(infoText)) 882 883 if self.infoFile is not None: 884 with open(self.infoFile, "w", encoding="UTF-8") as fH: 885 fH.write(infoText) 886 887 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 888 889 if self.useHTMLReports: 890 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 891 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 892 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 893 894 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 895 896 return infoText 897 898 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 899 """ 900 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 901 902 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 903 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 904 :return: JSON formatted data with information about instrument. 905 """ 906 tickerJSON = {} 907 if self.moreDebug: 908 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 909 910 if not self._ticker: 911 uLogger.warning("self._ticker variable is not be empty!") 912 913 else: 914 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 915 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 916 raise Exception("Instrument not allowed") 917 918 if not self.iList: 919 self.iList = self.Listing() 920 921 if self._ticker in self.iList["Shares"].keys(): 922 tickerJSON = self.iList["Shares"][self._ticker] 923 if self.moreDebug: 924 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 925 926 elif self._ticker in self.iList["Currencies"].keys(): 927 tickerJSON = self.iList["Currencies"][self._ticker] 928 if self.moreDebug: 929 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 930 931 elif self._ticker in self.iList["Bonds"].keys(): 932 tickerJSON = self.iList["Bonds"][self._ticker] 933 if self.moreDebug: 934 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 935 936 elif self._ticker in self.iList["Etfs"].keys(): 937 tickerJSON = self.iList["Etfs"][self._ticker] 938 if self.moreDebug: 939 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 940 941 elif self._ticker in self.iList["Futures"].keys(): 942 tickerJSON = self.iList["Futures"][self._ticker] 943 if self.moreDebug: 944 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 945 946 if tickerJSON: 947 self._figi = tickerJSON["figi"] 948 949 if requestPrice: 950 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 951 952 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 953 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 954 955 else: 956 tickerJSON["currentPrice"]["changes"] = 0 957 958 if show: 959 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 960 961 else: 962 if show: 963 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 964 965 return tickerJSON 966 967 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 968 """ 969 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 970 971 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 972 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 973 :return: JSON formatted data with information about instrument. 974 """ 975 figiJSON = {} 976 if self.moreDebug: 977 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 978 979 if not self._figi: 980 uLogger.warning("self._figi variable is not be empty!") 981 982 else: 983 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 984 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 985 raise Exception("Instrument not allowed") 986 987 if not self.iList: 988 self.iList = self.Listing() 989 990 for item in self.iList["Shares"].keys(): 991 if self._figi == self.iList["Shares"][item]["figi"]: 992 figiJSON = self.iList["Shares"][item] 993 994 if self.moreDebug: 995 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 996 997 break 998 999 if not figiJSON: 1000 for item in self.iList["Currencies"].keys(): 1001 if self._figi == self.iList["Currencies"][item]["figi"]: 1002 figiJSON = self.iList["Currencies"][item] 1003 1004 if self.moreDebug: 1005 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1006 1007 break 1008 1009 if not figiJSON: 1010 for item in self.iList["Bonds"].keys(): 1011 if self._figi == self.iList["Bonds"][item]["figi"]: 1012 figiJSON = self.iList["Bonds"][item] 1013 1014 if self.moreDebug: 1015 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1016 1017 break 1018 1019 if not figiJSON: 1020 for item in self.iList["Etfs"].keys(): 1021 if self._figi == self.iList["Etfs"][item]["figi"]: 1022 figiJSON = self.iList["Etfs"][item] 1023 1024 if self.moreDebug: 1025 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1026 1027 break 1028 1029 if not figiJSON: 1030 for item in self.iList["Futures"].keys(): 1031 if self._figi == self.iList["Futures"][item]["figi"]: 1032 figiJSON = self.iList["Futures"][item] 1033 1034 if self.moreDebug: 1035 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1036 1037 break 1038 1039 if figiJSON: 1040 self._figi = figiJSON["figi"] 1041 self._ticker = figiJSON["ticker"] 1042 1043 if requestPrice: 1044 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1045 1046 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1047 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1048 1049 else: 1050 figiJSON["currentPrice"]["changes"] = 0 1051 1052 if show: 1053 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1054 1055 else: 1056 if show: 1057 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1058 1059 return figiJSON 1060 1061 def GetCurrentPrices(self, show: bool = True) -> dict: 1062 """ 1063 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1064 `{"buy": [{"price": 1243.8, "quantity": 193}, 1065 {"price": 1244.0, "quantity": 168}, 1066 {"price": 1244.8, "quantity": 5}, 1067 {"price": 1245.0, "quantity": 61}, 1068 {"price": 1245.4, "quantity": 60}], 1069 "sell": [{"price": 1243.6, "quantity": 8}, 1070 {"price": 1242.6, "quantity": 10}, 1071 {"price": 1242.4, "quantity": 18}, 1072 {"price": 1242.2, "quantity": 50}, 1073 {"price": 1242.0, "quantity": 113}], 1074 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1075 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1076 - sell: list of dicts with Buyers prices, 1077 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1078 - quantity: volume value by current price in lots, 1079 - limitUp: current trade session limit price, maximum, 1080 - limitDown: current trade session limit price, minimum, 1081 - lastPrice: last deal price of the instrument, 1082 - closePrice: previous trade session close price of the instrument. 1083 1084 See also: `SearchByTicker()` and `SearchByFIGI()`. 1085 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1086 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1087 1088 :param show: if `True` then print DOM to log and console. 1089 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1090 If an error occurred then returns an empty record: 1091 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1092 """ 1093 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1094 1095 if self.depth < 1: 1096 uLogger.error("Depth of Market (DOM) must be >=1!") 1097 raise Exception("Incorrect value") 1098 1099 if not (self._ticker or self._figi): 1100 uLogger.error("self._ticker or self._figi variables must be defined!") 1101 raise Exception("Ticker or FIGI required") 1102 1103 if self._ticker and not self._figi: 1104 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1105 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1106 1107 if not self._ticker and self._figi: 1108 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1109 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1110 1111 if not self._figi: 1112 uLogger.error("FIGI is not defined!") 1113 raise Exception("Ticker or FIGI required") 1114 1115 else: 1116 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1117 1118 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1119 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1120 self.body = str({"figi": self._figi, "depth": self.depth}) 1121 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1122 1123 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1124 # list of dicts with sellers orders: 1125 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1126 1127 # list of dicts with buyers orders: 1128 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1129 1130 # max price of instrument at this time: 1131 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1132 1133 # min price of instrument at this time: 1134 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1135 1136 # last price of deal with instrument: 1137 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1138 1139 # last close price of instrument: 1140 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1141 1142 else: 1143 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1144 uLogger.debug("Server response: {}".format(pricesResponse)) 1145 1146 if show: 1147 if prices["buy"] or prices["sell"]: 1148 info = [ 1149 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1150 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1151 self._ticker, 1152 self._figi, 1153 self.depth, 1154 ), 1155 "-" * 60, "\n", 1156 " Orders of Buyers | Orders of Sellers\n", 1157 "-" * 60, "\n", 1158 " Sell prices (volumes) | Buy prices (volumes)\n", 1159 "-" * 60, "\n", 1160 ] 1161 1162 if not prices["buy"]: 1163 info.append(" | No orders!\n") 1164 sumBuy = 0 1165 1166 else: 1167 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1168 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1169 for item in maxMinSorted: 1170 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1171 1172 if not prices["sell"]: 1173 info.append("No orders! |\n") 1174 sumSell = 0 1175 1176 else: 1177 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1178 for item in prices["sell"]: 1179 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1180 1181 info.extend([ 1182 "-" * 60, "\n", 1183 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1184 "-" * 60, "\n", 1185 ]) 1186 1187 infoText = "".join(info) 1188 1189 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1190 1191 else: 1192 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1193 1194 return prices 1195 1196 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1197 """ 1198 This method get and show information about all available broker instruments for current user account. 1199 If `instrumentsFile` string is not empty then also save information to this file. 1200 1201 :param show: if `True` then print results to console, if `False` — print only to file. 1202 :return: multi-lines string with all available broker instruments 1203 """ 1204 if not self.iList: 1205 self.iList = self.Listing() 1206 1207 info = [ 1208 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1209 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1210 ] 1211 1212 # add instruments count by type: 1213 for iType in self.iList.keys(): 1214 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1215 1216 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1217 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1218 1219 # generating info tables with all instruments by type: 1220 for iType in self.iList.keys(): 1221 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1222 1223 for instrument in self.iList[iType].keys(): 1224 iName = self.iList[iType][instrument]["name"] # instrument's name 1225 if len(iName) > 57: 1226 iName = "{}...".format(iName[:54]) # right trim for a long string 1227 1228 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1229 self.iList[iType][instrument]["ticker"], 1230 iName, 1231 self.iList[iType][instrument]["figi"], 1232 self.iList[iType][instrument]["currency"], 1233 self.iList[iType][instrument]["lot"], 1234 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1235 )) 1236 1237 infoText = "".join(info) 1238 1239 if show: 1240 uLogger.info(infoText) 1241 1242 if self.instrumentsFile: 1243 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1244 fH.write(infoText) 1245 1246 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1247 1248 if self.useHTMLReports: 1249 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1250 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1251 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1252 1253 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1254 1255 return infoText 1256 1257 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1258 """ 1259 This method search and show information about instruments by part of its ticker, FIGI or name. 1260 If `searchResultsFile` string is not empty then also save information to this file. 1261 1262 :param pattern: string with part of ticker, FIGI or instrument's name. 1263 :param show: if `True` then print results to console, if `False` — return list of result only. 1264 :return: list of dictionaries with all found instruments. 1265 """ 1266 if not self.iList: 1267 self.iList = self.Listing() 1268 1269 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1270 compiledPattern = re.compile(pattern, re.IGNORECASE) 1271 1272 for iType in self.iList: 1273 for instrument in self.iList[iType].values(): 1274 searchResult = compiledPattern.search(" ".join( 1275 [instrument["ticker"], instrument["figi"], instrument["name"]] 1276 )) 1277 1278 if searchResult: 1279 searchResults[iType][instrument["ticker"]] = instrument 1280 1281 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1282 info = [ 1283 "# Search results\n\n", 1284 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1285 "* **Search pattern:** [{}]\n".format(pattern), 1286 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1287 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1288 ] 1289 infoShort = info[:] 1290 1291 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1292 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1293 skippedLine = "| ... | ... | ... | ... |\n" 1294 1295 if resultsLen == 0: 1296 info.append("\nNo results\n") 1297 infoShort.append("\nNo results\n") 1298 uLogger.warning("No results. Try changing your search pattern.") 1299 1300 else: 1301 for iType in searchResults: 1302 iTypeValuesCount = len(searchResults[iType].values()) 1303 if iTypeValuesCount > 0: 1304 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1305 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1306 1307 for instrument in searchResults[iType].values(): 1308 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1309 instrument["type"], 1310 instrument["ticker"], 1311 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1312 instrument["figi"], 1313 )) 1314 1315 if iTypeValuesCount <= 5: 1316 infoShort.extend(info[-iTypeValuesCount:]) 1317 1318 else: 1319 infoShort.extend(info[-5:]) 1320 infoShort.append(skippedLine) 1321 1322 infoText = "".join(info) 1323 infoTextShort = "".join(infoShort) 1324 1325 if show: 1326 uLogger.info(infoTextShort) 1327 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1328 1329 if self.searchResultsFile: 1330 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1331 fH.write(infoText) 1332 1333 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1334 1335 if self.useHTMLReports: 1336 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1337 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1338 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1339 1340 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1341 1342 return searchResults 1343 1344 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1345 """ 1346 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1347 1348 :param instruments: list of strings with tickers or FIGIs. 1349 :return: list with unique instrument FIGIs only. 1350 """ 1351 requestedInstruments = [] 1352 for iName in instruments: 1353 if iName not in self.aliases.keys(): 1354 if iName not in requestedInstruments: 1355 requestedInstruments.append(iName) 1356 1357 else: 1358 if iName not in requestedInstruments: 1359 if self.aliases[iName] not in requestedInstruments: 1360 requestedInstruments.append(self.aliases[iName]) 1361 1362 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1363 1364 onlyUniqueFIGIs = [] 1365 for iName in requestedInstruments: 1366 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1367 continue 1368 1369 self._ticker = iName 1370 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1371 1372 if not iData: 1373 self._ticker = "" 1374 self._figi = iName 1375 1376 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1377 1378 if not iData: 1379 self._figi = "" 1380 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1381 1382 if iData and iData["figi"] not in onlyUniqueFIGIs: 1383 onlyUniqueFIGIs.append(iData["figi"]) 1384 1385 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1386 1387 return onlyUniqueFIGIs 1388 1389 def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]: 1390 """ 1391 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1392 1393 See limits: https://tinkoff.github.io/investAPI/limits/ 1394 1395 If `pricesFile` string is not empty then also save information to this file. 1396 1397 :param instruments: list of strings with tickers or FIGIs. 1398 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1399 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1400 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1401 """ 1402 if instruments is None or not instruments: 1403 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1404 raise Exception("Ticker or FIGI required") 1405 1406 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1407 1408 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1409 1410 iList = [] # trying to get info and current prices about all unique instruments: 1411 for self._figi in onlyUniqueFIGIs: 1412 iData = self.SearchByFIGI(requestPrice=True) 1413 iList.append(iData) 1414 1415 self.ShowListOfPrices(iList, show) 1416 1417 return iList 1418 1419 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1420 """ 1421 Show table contains current prices of given instruments. 1422 1423 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1424 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1425 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1426 :return: multilines text in Markdown format as a table contains current prices. 1427 """ 1428 infoText = "" 1429 1430 if show or self.pricesFile: 1431 info = [ 1432 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1433 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1434 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1435 ] 1436 1437 for item in iList: 1438 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1439 item["ticker"], 1440 item["figi"], 1441 item["type"], 1442 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1443 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1444 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1445 "{} / {}".format( 1446 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1447 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1448 ), 1449 "{} / {}".format( 1450 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1451 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1452 ), 1453 item["currency"], 1454 )) 1455 1456 infoText = "".join(info) 1457 1458 if show: 1459 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1460 1461 if self.pricesFile: 1462 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1463 fH.write(infoText) 1464 1465 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1466 1467 if self.useHTMLReports: 1468 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1469 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1470 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1471 1472 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1473 1474 return infoText 1475 1476 def RequestTradingStatus(self) -> dict: 1477 """ 1478 Requesting trading status for the instrument defined by `figi` variable. 1479 1480 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1481 1482 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1483 1484 :return: dictionary with trading status attributes. Response example: 1485 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1486 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1487 """ 1488 if self._figi is None or not self._figi: 1489 uLogger.error("Variable `figi` must be defined for using this method!") 1490 raise Exception("FIGI required") 1491 1492 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1493 1494 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1495 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1496 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1497 1498 if self.moreDebug: 1499 uLogger.debug("Records about current trading status successfully received") 1500 1501 return tradingStatus 1502 1503 def RequestPortfolio(self) -> dict: 1504 """ 1505 Requesting actual user's portfolio for current `accountId`. 1506 1507 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1508 1509 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1510 1511 :return: dictionary with user's portfolio. 1512 """ 1513 if self.accountId is None or not self.accountId: 1514 uLogger.error("Variable `accountId` must be defined for using this method!") 1515 raise Exception("Account ID required") 1516 1517 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1518 1519 self.body = str({"accountId": self.accountId}) 1520 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1521 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1522 1523 if self.moreDebug: 1524 uLogger.debug("Records about user's portfolio successfully received") 1525 1526 return rawPortfolio 1527 1528 def RequestPositions(self) -> dict: 1529 """ 1530 Requesting open positions by currencies and instruments for current `accountId`. 1531 1532 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1533 1534 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1535 1536 :return: dictionary with open positions by instruments. 1537 """ 1538 if self.accountId is None or not self.accountId: 1539 uLogger.error("Variable `accountId` must be defined for using this method!") 1540 raise Exception("Account ID required") 1541 1542 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1543 1544 self.body = str({"accountId": self.accountId}) 1545 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1546 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1547 1548 if self.moreDebug: 1549 uLogger.debug("Records about current open positions successfully received") 1550 1551 return rawPositions 1552 1553 def RequestPendingOrders(self) -> list: 1554 """ 1555 Requesting current actual pending limit orders for current `accountId`. 1556 1557 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1558 1559 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1560 1561 :return: list of dictionaries with pending limit orders. 1562 """ 1563 if self.accountId is None or not self.accountId: 1564 uLogger.error("Variable `accountId` must be defined for using this method!") 1565 raise Exception("Account ID required") 1566 1567 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1568 1569 self.body = str({"accountId": self.accountId}) 1570 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1571 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1572 1573 if "orders" in rawResponse.keys(): 1574 rawOrders = rawResponse["orders"] 1575 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1576 1577 else: 1578 rawOrders = [] 1579 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1580 1581 return rawOrders 1582 1583 def RequestStopOrders(self) -> list: 1584 """ 1585 Requesting current actual stop orders for current `accountId`. 1586 1587 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1588 1589 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1590 1591 :return: list of dictionaries with stop orders. 1592 """ 1593 if self.accountId is None or not self.accountId: 1594 uLogger.error("Variable `accountId` must be defined for using this method!") 1595 raise Exception("Account ID required") 1596 1597 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1598 1599 self.body = str({"accountId": self.accountId}) 1600 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1601 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1602 1603 if "stopOrders" in rawResponse.keys(): 1604 rawStopOrders = rawResponse["stopOrders"] 1605 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1606 1607 else: 1608 rawStopOrders = [] 1609 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1610 1611 return rawStopOrders 1612 1613 def Overview(self, show: bool = False, details: str = "full") -> dict: 1614 """ 1615 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1616 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1617 and `overviewBondsCalendarFile` are defined then also save information to file. 1618 1619 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1620 many requests about the state of the portfolio, and then, based on the received data, a large number 1621 of calculation and statistics are collected. 1622 1623 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1624 :param details: how detailed should the information be? 1625 - `full` — shows full available information about portfolio status (by default), 1626 - `positions` — shows only open positions, 1627 - `orders` — shows only sections of open limits and stop orders. 1628 - `digest` — show a short digest of the portfolio status, 1629 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1630 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1631 :return: dictionary with client's raw portfolio and some statistics. 1632 """ 1633 if self.accountId is None or not self.accountId: 1634 uLogger.error("Variable `accountId` must be defined for using this method!") 1635 raise Exception("Account ID required") 1636 1637 view = { 1638 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1639 "headers": {}, # list of dictionaries, response headers without "positions" section 1640 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1641 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1642 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1643 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1644 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1645 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1646 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1647 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1648 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1649 }, 1650 "stat": { # --- some statistics calculated using "raw" sections: 1651 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1652 "availableRUB": 0., # available rubles (without other currencies) 1653 "blockedRUB": 0., # blocked sum in Russian Rouble 1654 "totalChangesRUB": 0., # changes for all open trades in RUB 1655 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1656 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1657 "sharesCostRUB": 0., # costs of all shares in RUB 1658 "bondsCostRUB": 0., # costs of all bonds in RUB 1659 "etfsCostRUB": 0., # costs of all etfs in RUB 1660 "futuresCostRUB": 0., # costs of all futures in RUB 1661 "Currencies": [], # list of dictionaries of all currencies statistics 1662 "Shares": [], # list of dictionaries of all shares statistics 1663 "Bonds": [], # list of dictionaries of all bonds statistics 1664 "Etfs": [], # list of dictionaries of all etfs statistics 1665 "Futures": [], # list of dictionaries of all futures statistics 1666 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1667 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1668 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1669 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1670 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1671 }, 1672 "analytics": { # --- some analytics of portfolio: 1673 "distrByAssets": {}, # portfolio distribution by assets 1674 "distrByCompanies": {}, # portfolio distribution by companies 1675 "distrBySectors": {}, # portfolio distribution by sectors 1676 "distrByCurrencies": {}, # portfolio distribution by currencies 1677 "distrByCountries": {}, # portfolio distribution by countries 1678 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1679 } 1680 } 1681 1682 details = details.lower() 1683 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1684 if details not in availableDetails: 1685 details = "full" 1686 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1687 1688 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1689 1690 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1691 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1692 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1693 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1694 1695 # save response headers without "positions" section: 1696 for key in portfolioResponse.keys(): 1697 if key != "positions": 1698 view["raw"]["headers"][key] = portfolioResponse[key] 1699 1700 else: 1701 continue 1702 1703 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1704 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1705 for item in portfolioResponse["positions"]: 1706 if item["instrumentType"] == "currency": 1707 self._figi = item["figi"] 1708 if not self._figi and item["ticker"]: 1709 self._ticker = item["ticker"] 1710 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1711 1712 curr = self.SearchByFIGI(requestPrice=False) 1713 1714 # current price of currency in RUB: 1715 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1716 "name": curr["name"], 1717 "currentPrice": NanoToFloat( 1718 item["currentPrice"]["units"], 1719 item["currentPrice"]["nano"] 1720 ), 1721 } 1722 1723 view["raw"]["Currencies"].append(item) 1724 1725 elif item["instrumentType"] == "share": 1726 view["raw"]["Shares"].append(item) 1727 1728 elif item["instrumentType"] == "bond": 1729 view["raw"]["Bonds"].append(item) 1730 1731 elif item["instrumentType"] == "etf": 1732 view["raw"]["Etfs"].append(item) 1733 1734 elif item["instrumentType"] == "futures": 1735 view["raw"]["Futures"].append(item) 1736 1737 else: 1738 continue 1739 1740 # how many volume of currencies (by ISO currency name) are blocked: 1741 for item in view["raw"]["positions"]["blocked"]: 1742 blocked = NanoToFloat(item["units"], item["nano"]) 1743 if blocked > 0: 1744 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1745 1746 # how many volume of instruments (by FIGI) are blocked: 1747 for item in view["raw"]["positions"]["securities"]: 1748 blocked = int(item["blocked"]) 1749 if blocked > 0: 1750 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1751 1752 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1753 1754 if "rub" in allBlocked.keys(): 1755 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1756 1757 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1758 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1759 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1760 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1761 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1762 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1763 view["stat"]["portfolioCostRUB"] = sum([ 1764 view["stat"]["allCurrenciesCostRUB"], 1765 view["stat"]["sharesCostRUB"], 1766 view["stat"]["bondsCostRUB"], 1767 view["stat"]["etfsCostRUB"], 1768 view["stat"]["futuresCostRUB"], 1769 ]) 1770 1771 # --- calculating some portfolio statistics: 1772 byComp = {} # distribution by companies 1773 bySect = {} # distribution by sectors 1774 byCurr = {} # distribution by currencies (include RUB) 1775 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1776 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1777 1778 for item in portfolioResponse["positions"]: 1779 self._figi = item["figi"] 1780 if not self._figi and item["ticker"]: 1781 self._ticker = item["ticker"] 1782 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1783 1784 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1785 1786 if instrument: 1787 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1788 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1789 1790 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1791 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1792 1793 else: 1794 blocked = 0 1795 1796 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1797 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1798 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1799 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1800 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1801 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1802 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1803 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1804 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1805 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1806 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1807 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1808 1809 statData = { 1810 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1811 "ticker": instrument["ticker"], # ticker by FIGI 1812 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1813 "volume": volume, # available volume of instrument 1814 "lots": lots, # volume in lots of instrument 1815 "direction": direction, # direction of an instrument's position: short or long 1816 "blocked": blocked, # blocked volume of currency or instrument 1817 "currentPrice": curPrice, # current instrument's price in basic asset 1818 "average": average, # current average position price 1819 "cost": cost, # current cost of all volume of instrument in basic asset 1820 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1821 "costRUB": costRUB, # cost of instrument in ruble 1822 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1823 "profit": profit, # expected profit at current moment 1824 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1825 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1826 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1827 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1828 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1829 "step": instrument["step"], # minimum price increment 1830 } 1831 1832 # adding distribution by unique countries: 1833 if statData["country"] not in byCountry.keys(): 1834 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1835 1836 else: 1837 byCountry[statData["country"]]["cost"] += costRUB 1838 byCountry[statData["country"]]["percent"] += percentCostRUB 1839 1840 if item["instrumentType"] != "currency": 1841 # adding distribution by unique companies: 1842 if statData["name"]: 1843 if statData["name"] not in byComp.keys(): 1844 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1845 1846 else: 1847 byComp[statData["name"]]["cost"] += costRUB 1848 byComp[statData["name"]]["percent"] += percentCostRUB 1849 1850 # adding distribution by unique sectors: 1851 if statData["sector"] not in bySect.keys(): 1852 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1853 1854 else: 1855 bySect[statData["sector"]]["cost"] += costRUB 1856 bySect[statData["sector"]]["percent"] += percentCostRUB 1857 1858 # adding distribution by unique currencies: 1859 if currency not in byCurr.keys(): 1860 byCurr[currency] = { 1861 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1862 "cost": costRUB, 1863 "percent": percentCostRUB 1864 } 1865 1866 else: 1867 byCurr[currency]["cost"] += costRUB 1868 byCurr[currency]["percent"] += percentCostRUB 1869 1870 # saving statistics for every instrument: 1871 if item["instrumentType"] == "currency": 1872 view["stat"]["Currencies"].append(statData) 1873 1874 # update dict with free funds for trading (total - blocked) by currencies 1875 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1876 view["stat"]["funds"][currency] = { 1877 "total": volume, 1878 "totalCostRUB": costRUB, # total volume cost in rubles 1879 "free": volume - blocked, 1880 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1881 } 1882 1883 elif item["instrumentType"] == "share": 1884 view["stat"]["Shares"].append(statData) 1885 1886 elif item["instrumentType"] == "bond": 1887 view["stat"]["Bonds"].append(statData) 1888 1889 elif item["instrumentType"] == "etf": 1890 view["stat"]["Etfs"].append(statData) 1891 1892 elif item["instrumentType"] == "Futures": 1893 view["stat"]["Futures"].append(statData) 1894 1895 else: 1896 continue 1897 1898 # total changes in Russian Ruble: 1899 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1900 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1901 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1902 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1903 view["stat"]["funds"]["rub"] = { 1904 "total": view["stat"]["availableRUB"], 1905 "totalCostRUB": view["stat"]["availableRUB"], 1906 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1907 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1908 } 1909 1910 # --- pending limit orders sector data: 1911 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1912 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1913 1914 for item in view["raw"]["orders"]: 1915 self._figi = item["figi"] 1916 1917 if item["figi"] not in uniquePendingOrdersFIGIs: 1918 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1919 1920 uniquePendingOrdersFIGIs.append(item["figi"]) 1921 uniquePendingOrders[item["figi"]] = instrument 1922 1923 else: 1924 instrument = uniquePendingOrders[item["figi"]] 1925 1926 if instrument: 1927 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1928 orderType = TKS_ORDER_TYPES[item["orderType"]] 1929 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1930 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1931 1932 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1933 if item["direction"] == "ORDER_DIRECTION_BUY": 1934 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1935 1936 else: 1937 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1938 1939 # requested price for order execution: 1940 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1941 1942 # necessary changes in percent to reach target from current price: 1943 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1944 1945 view["stat"]["orders"].append({ 1946 "orderID": item["orderId"], # orderId number parameter of current order 1947 "figi": item["figi"], # FIGI identification 1948 "ticker": instrument["ticker"], # ticker name by FIGI 1949 "lotsRequested": item["lotsRequested"], # requested lots value 1950 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1951 "currentPrice": lastPrice, # current instrument's price for defined action 1952 "targetPrice": target, # requested price for order execution in base currency 1953 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1954 "percentChanges": changes, # changes in percent to target from current price 1955 "currency": item["currency"], # instrument's currency name 1956 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1957 "type": orderType, # type of order from TKS_ORDER_TYPES 1958 "status": orderState, # order status from TKS_ORDER_STATES 1959 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1960 }) 1961 1962 # --- stop orders sector data: 1963 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1964 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1965 1966 for item in view["raw"]["stopOrders"]: 1967 self._figi = item["figi"] 1968 1969 if item["figi"] not in uniqueStopOrdersFIGIs: 1970 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1971 1972 uniqueStopOrdersFIGIs.append(item["figi"]) 1973 uniqueStopOrders[item["figi"]] = instrument 1974 1975 else: 1976 instrument = uniqueStopOrders[item["figi"]] 1977 1978 if instrument: 1979 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1980 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1981 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1982 1983 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1984 if "expirationTime" in item.keys(): 1985 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1986 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1987 1988 else: 1989 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1990 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1991 1992 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1993 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1994 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1995 1996 else: 1997 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1998 1999 # requested price when stop-order executed: 2000 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2001 2002 # price for limit-order, set up when stop-order executed: 2003 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2004 2005 # necessary changes in percent to reach target from current price: 2006 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2007 2008 view["stat"]["stopOrders"].append({ 2009 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2010 "figi": item["figi"], # FIGI identification 2011 "ticker": instrument["ticker"], # ticker name by FIGI 2012 "lotsRequested": item["lotsRequested"], # requested lots value 2013 "currentPrice": lastPrice, # current instrument's price for defined action 2014 "targetPrice": target, # requested price for stop-order execution in base currency 2015 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2016 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2017 "percentChanges": changes, # changes in percent to target from current price 2018 "currency": item["currency"], # instrument's currency name 2019 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2020 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2021 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2022 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2023 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2024 }) 2025 2026 # --- calculating data for analytics section: 2027 # portfolio distribution by assets: 2028 view["analytics"]["distrByAssets"] = { 2029 "Ruble": { 2030 "uniques": 1, 2031 "cost": view["stat"]["availableRUB"], 2032 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2033 }, 2034 "Currencies": { 2035 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2036 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2037 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2038 }, 2039 "Shares": { 2040 "uniques": len(view["stat"]["Shares"]), 2041 "cost": view["stat"]["sharesCostRUB"], 2042 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2043 }, 2044 "Bonds": { 2045 "uniques": len(view["stat"]["Bonds"]), 2046 "cost": view["stat"]["bondsCostRUB"], 2047 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2048 }, 2049 "Etfs": { 2050 "uniques": len(view["stat"]["Etfs"]), 2051 "cost": view["stat"]["etfsCostRUB"], 2052 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2053 }, 2054 "Futures": { 2055 "uniques": len(view["stat"]["Futures"]), 2056 "cost": view["stat"]["futuresCostRUB"], 2057 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2058 }, 2059 } 2060 2061 # portfolio distribution by companies: 2062 view["analytics"]["distrByCompanies"]["All money cash"] = { 2063 "ticker": "", 2064 "cost": view["stat"]["allCurrenciesCostRUB"], 2065 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2066 } 2067 view["analytics"]["distrByCompanies"].update(byComp) 2068 2069 # portfolio distribution by sectors: 2070 view["analytics"]["distrBySectors"]["All money cash"] = { 2071 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2072 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2073 } 2074 view["analytics"]["distrBySectors"].update(bySect) 2075 2076 # portfolio distribution by currencies: 2077 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2078 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2079 2080 if self.moreDebug: 2081 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2082 2083 view["analytics"]["distrByCurrencies"].update(byCurr) 2084 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2085 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2086 2087 # portfolio distribution by countries: 2088 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2089 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2090 2091 if self.moreDebug: 2092 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2093 2094 view["analytics"]["distrByCountries"].update(byCountry) 2095 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2096 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2097 2098 # --- Prepare text statistics overview in human-readable: 2099 if show: 2100 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2101 2102 # Whatever the value `details`, header not changes: 2103 info = [ 2104 "# Client's portfolio\n\n", 2105 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2106 "* **Account ID:** [{}]\n".format(self.accountId), 2107 ] 2108 2109 if details in ["full", "positions", "digest"]: 2110 info.extend([ 2111 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2112 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2113 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2114 view["stat"]["totalChangesRUB"], 2115 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2116 view["stat"]["totalChangesPercentRUB"], 2117 ), 2118 ]) 2119 2120 if details in ["full", "positions"]: 2121 info.extend([ 2122 "## Open positions\n\n", 2123 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2124 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2125 "| **Ruble:** | {:>31} | | | | | |\n".format( 2126 "{:.2f} ({:.2f}) rub".format( 2127 view["stat"]["availableRUB"], 2128 view["stat"]["blockedRUB"], 2129 ) 2130 ) 2131 ]) 2132 2133 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2134 return [ 2135 "| | | | | | | |\n", 2136 "| {:<27} | | | | | {:>19} | |\n".format( 2137 noTradeStr if noTradeStr else typeStr, 2138 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2139 ), 2140 ] 2141 2142 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2143 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2144 "{} [{}]".format(data["ticker"], data["figi"]), 2145 "{:.2f} ({:.2f}) {}".format( 2146 data["volume"], 2147 data["blocked"], 2148 data["currency"], 2149 ) if isCurr else "{:.0f} ({:.0f})".format( 2150 data["volume"], 2151 data["blocked"], 2152 ), 2153 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2154 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2155 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2156 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2157 "{}{:.2f} {} ({}{:.2f}%)".format( 2158 "+" if data["profit"] > 0 else "", 2159 data["profit"], data["baseCurrencyName"], 2160 "+" if data["percentProfit"] > 0 else "", 2161 data["percentProfit"], 2162 ), 2163 ) 2164 2165 # --- Show currencies section: 2166 if view["stat"]["Currencies"]: 2167 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2168 for item in view["stat"]["Currencies"]: 2169 info.append(_InfoStr(item, isCurr=True)) 2170 2171 else: 2172 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2173 2174 # --- Show shares section: 2175 if view["stat"]["Shares"]: 2176 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2177 2178 for item in view["stat"]["Shares"]: 2179 info.append(_InfoStr(item)) 2180 2181 else: 2182 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2183 2184 # --- Show bonds section: 2185 if view["stat"]["Bonds"]: 2186 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2187 2188 for item in view["stat"]["Bonds"]: 2189 info.append(_InfoStr(item)) 2190 2191 else: 2192 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2193 2194 # --- Show etfs section: 2195 if view["stat"]["Etfs"]: 2196 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2197 2198 for item in view["stat"]["Etfs"]: 2199 info.append(_InfoStr(item)) 2200 2201 else: 2202 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2203 2204 # --- Show futures section: 2205 if view["stat"]["Futures"]: 2206 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2207 2208 for item in view["stat"]["Futures"]: 2209 info.append(_InfoStr(item)) 2210 2211 else: 2212 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2213 2214 if details in ["full", "orders"]: 2215 # --- Show pending limit orders section: 2216 if view["stat"]["orders"]: 2217 info.extend([ 2218 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2219 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2220 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2221 ]) 2222 2223 for item in view["stat"]["orders"]: 2224 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2225 "{} [{}]".format(item["ticker"], item["figi"]), 2226 item["orderID"], 2227 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2228 "{} {} ({}{:.2f}%)".format( 2229 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2230 item["baseCurrencyName"], 2231 "+" if item["percentChanges"] > 0 else "", 2232 float(item["percentChanges"]), 2233 ), 2234 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2235 item["action"], 2236 item["type"], 2237 item["date"], 2238 )) 2239 2240 else: 2241 info.append("\n## Total pending limit-orders: [0]\n") 2242 2243 # --- Show stop orders section: 2244 if view["stat"]["stopOrders"]: 2245 info.extend([ 2246 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2247 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2248 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2249 ]) 2250 2251 for item in view["stat"]["stopOrders"]: 2252 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2253 "{} [{}]".format(item["ticker"], item["figi"]), 2254 item["orderID"], 2255 item["lotsRequested"], 2256 "{} {} ({}{:.2f}%)".format( 2257 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2258 item["baseCurrencyName"], 2259 "+" if item["percentChanges"] > 0 else "", 2260 float(item["percentChanges"]), 2261 ), 2262 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2263 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2264 item["action"], 2265 item["type"], 2266 item["expType"], 2267 item["createDate"], 2268 item["expDate"], 2269 )) 2270 2271 else: 2272 info.append("\n## Total stop-orders: [0]\n") 2273 2274 if details in ["full", "analytics"]: 2275 # -- Show analytics section: 2276 if view["stat"]["portfolioCostRUB"] > 0: 2277 info.extend([ 2278 "\n# Analytics\n\n" 2279 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2280 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2281 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2282 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2283 view["stat"]["totalChangesRUB"], 2284 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2285 view["stat"]["totalChangesPercentRUB"], 2286 ), 2287 "\n## Portfolio distribution by assets\n" 2288 "\n| Type | Uniques | Percent | Current cost |\n", 2289 "|------------------------------------|---------|---------|--------------------|\n", 2290 ]) 2291 2292 for key in view["analytics"]["distrByAssets"].keys(): 2293 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2294 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2295 key, 2296 view["analytics"]["distrByAssets"][key]["uniques"], 2297 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2298 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2299 )) 2300 2301 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2302 2303 info.extend([ 2304 "\n## Portfolio distribution by companies\n" 2305 "\n| Company | Percent | Current cost |\n", 2306 aSepLine, 2307 ]) 2308 2309 for company in view["analytics"]["distrByCompanies"].keys(): 2310 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2311 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2312 "{}{}".format( 2313 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2314 company, 2315 ), 2316 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2317 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2318 )) 2319 2320 info.extend([ 2321 "\n## Portfolio distribution by sectors\n" 2322 "\n| Sector | Percent | Current cost |\n", 2323 aSepLine, 2324 ]) 2325 2326 for sector in view["analytics"]["distrBySectors"].keys(): 2327 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2328 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2329 sector, 2330 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2331 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2332 )) 2333 2334 info.extend([ 2335 "\n## Portfolio distribution by currencies\n" 2336 "\n| Instruments currencies | Percent | Current cost |\n", 2337 aSepLine, 2338 ]) 2339 2340 for curr in view["analytics"]["distrByCurrencies"].keys(): 2341 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2342 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2343 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2344 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2345 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2346 )) 2347 2348 info.extend([ 2349 "\n## Portfolio distribution by countries\n" 2350 "\n| Assets by country | Percent | Current cost |\n", 2351 aSepLine, 2352 ]) 2353 2354 for country in view["analytics"]["distrByCountries"].keys(): 2355 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2356 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2357 country, 2358 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2359 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2360 )) 2361 2362 if details in ["full", "calendar"]: 2363 # -- Show bonds payment calendar section: 2364 if view["stat"]["Bonds"]: 2365 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2366 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2367 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2368 2369 else: 2370 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2371 2372 infoText = "".join(info) 2373 2374 uLogger.info(infoText) 2375 2376 if details == "full" and self.overviewFile: 2377 filename = self.overviewFile 2378 2379 elif details == "digest" and self.overviewDigestFile: 2380 filename = self.overviewDigestFile 2381 2382 elif details == "positions" and self.overviewPositionsFile: 2383 filename = self.overviewPositionsFile 2384 2385 elif details == "orders" and self.overviewOrdersFile: 2386 filename = self.overviewOrdersFile 2387 2388 elif details == "analytics" and self.overviewAnalyticsFile: 2389 filename = self.overviewAnalyticsFile 2390 2391 elif details == "calendar" and self.overviewBondsCalendarFile: 2392 filename = self.overviewBondsCalendarFile 2393 2394 else: 2395 filename = "" 2396 2397 if filename: 2398 with open(filename, "w", encoding="UTF-8") as fH: 2399 fH.write(infoText) 2400 2401 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2402 2403 if self.useHTMLReports: 2404 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2405 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2406 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2407 2408 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2409 2410 return view 2411 2412 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2413 """ 2414 Returns history operations between two given dates for current `accountId`. 2415 If `reportFile` string is not empty then also save human-readable report. 2416 Shows some statistical data of closed positions. 2417 2418 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2419 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2420 :param show: if `True` then also prints all records to the console. 2421 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2422 :return: original list of dictionaries with history of deals records from API ("operations" key): 2423 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2424 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2425 """ 2426 if self.accountId is None or not self.accountId: 2427 uLogger.error("Variable `accountId` must be defined for using this method!") 2428 raise Exception("Account ID required") 2429 2430 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2431 2432 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2433 2434 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2435 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2436 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2437 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2438 customStat = {} # custom statistics in additional to responseJSON 2439 2440 # --- output report in human-readable format: 2441 if show or self.reportFile: 2442 splitLine1 = "| | | | | |\n" # Summary section 2443 splitLine2 = "| | | | | | | | |\n" # Operations section 2444 nextDay = "" 2445 2446 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2447 2448 if len(ops) > 0: 2449 customStat = { 2450 "opsCount": 0, # total operations count 2451 "buyCount": 0, # buy operations 2452 "sellCount": 0, # sell operations 2453 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2454 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2455 "payIn": {"rub": 0.}, # Deposit brokerage account 2456 "payOut": {"rub": 0.}, # Withdrawals 2457 "divs": {"rub": 0.}, # Dividends income 2458 "coupons": {"rub": 0.}, # Coupon's income 2459 "brokerCom": {"rub": 0.}, # Service commissions 2460 "serviceCom": {"rub": 0.}, # Service commissions 2461 "marginCom": {"rub": 0.}, # Margin commissions 2462 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2463 } 2464 2465 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2466 for item in ops: 2467 if item["state"] == "OPERATION_STATE_EXECUTED": 2468 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2469 2470 # count buy operations: 2471 if "_BUY" in item["operationType"]: 2472 customStat["buyCount"] += 1 2473 2474 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2475 customStat["buyTotal"][item["payment"]["currency"]] += payment 2476 2477 else: 2478 customStat["buyTotal"][item["payment"]["currency"]] = payment 2479 2480 # count sell operations: 2481 elif "_SELL" in item["operationType"]: 2482 customStat["sellCount"] += 1 2483 2484 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2485 customStat["sellTotal"][item["payment"]["currency"]] += payment 2486 2487 else: 2488 customStat["sellTotal"][item["payment"]["currency"]] = payment 2489 2490 # count incoming operations: 2491 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2492 if item["payment"]["currency"] in customStat["payIn"].keys(): 2493 customStat["payIn"][item["payment"]["currency"]] += payment 2494 2495 else: 2496 customStat["payIn"][item["payment"]["currency"]] = payment 2497 2498 # count withdrawals operations: 2499 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2500 if item["payment"]["currency"] in customStat["payOut"].keys(): 2501 customStat["payOut"][item["payment"]["currency"]] += payment 2502 2503 else: 2504 customStat["payOut"][item["payment"]["currency"]] = payment 2505 2506 # count dividends income: 2507 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2508 if item["payment"]["currency"] in customStat["divs"].keys(): 2509 customStat["divs"][item["payment"]["currency"]] += payment 2510 2511 else: 2512 customStat["divs"][item["payment"]["currency"]] = payment 2513 2514 # count coupon's income: 2515 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2516 if item["payment"]["currency"] in customStat["coupons"].keys(): 2517 customStat["coupons"][item["payment"]["currency"]] += payment 2518 2519 else: 2520 customStat["coupons"][item["payment"]["currency"]] = payment 2521 2522 # count broker commissions: 2523 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2524 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2525 customStat["brokerCom"][item["payment"]["currency"]] += payment 2526 2527 else: 2528 customStat["brokerCom"][item["payment"]["currency"]] = payment 2529 2530 # count service commissions: 2531 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2532 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2533 customStat["serviceCom"][item["payment"]["currency"]] += payment 2534 2535 else: 2536 customStat["serviceCom"][item["payment"]["currency"]] = payment 2537 2538 # count margin commissions: 2539 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2540 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2541 customStat["marginCom"][item["payment"]["currency"]] += payment 2542 2543 else: 2544 customStat["marginCom"][item["payment"]["currency"]] = payment 2545 2546 # count withholding taxes: 2547 elif "_TAX" in item["operationType"]: 2548 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2549 customStat["allTaxes"][item["payment"]["currency"]] += payment 2550 2551 else: 2552 customStat["allTaxes"][item["payment"]["currency"]] = payment 2553 2554 else: 2555 continue 2556 2557 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2558 2559 # --- view "Actions" lines: 2560 info.extend([ 2561 "| Report sections | | | | |\n", 2562 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2563 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2564 "| | Buy: {:<22} | {:<28} | | |\n".format( 2565 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2566 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2567 ), 2568 "| | Sell: {:<21} | {:<28} | | |\n".format( 2569 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2570 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2571 ), 2572 ]) 2573 2574 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2575 for key in opsKeys: 2576 if key == "rub": 2577 continue 2578 2579 info.extend([ 2580 "| | | {:<28} | | |\n".format( 2581 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2582 ), 2583 "| | | {:<28} | | |\n".format( 2584 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2585 ), 2586 ]) 2587 2588 info.append(splitLine1) 2589 2590 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2591 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2592 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2593 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2594 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2595 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2596 ) 2597 2598 # --- view "Payments" lines: 2599 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2600 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2601 2602 for key in paymentsKeys: 2603 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2604 2605 info.append(splitLine1) 2606 2607 # --- view "Commissions and taxes" lines: 2608 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2609 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2610 2611 for key in comKeys: 2612 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2613 2614 info.extend([ 2615 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2616 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2617 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2618 ]) 2619 2620 else: 2621 info.append("Broker returned no operations during this period\n") 2622 2623 # --- view "Operations" section: 2624 for item in ops: 2625 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2626 continue 2627 2628 else: 2629 self._figi = item["figi"] 2630 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2631 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2632 2633 # group of deals during one day: 2634 if nextDay and item["date"].split("T")[0] != nextDay: 2635 info.append(splitLine2) 2636 nextDay = "" 2637 2638 else: 2639 nextDay = item["date"].split("T")[0] # saving current day for splitting 2640 2641 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2642 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2643 self._figi if self._figi else "—", 2644 instrument["ticker"] if instrument else "—", 2645 instrument["type"] if instrument else "—", 2646 item["quantity"] if int(item["quantity"]) > 0 else "—", 2647 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2648 TKS_OPERATION_STATES[item["state"]], 2649 TKS_OPERATION_TYPES[item["operationType"]], 2650 )) 2651 2652 infoText = "".join(info) 2653 2654 if show: 2655 if self.moreDebug: 2656 uLogger.debug("Records about history of a client's operations successfully received") 2657 2658 uLogger.info(infoText) 2659 2660 if self.reportFile: 2661 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2662 fH.write(infoText) 2663 2664 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2665 2666 if self.useHTMLReports: 2667 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2668 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2669 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2670 2671 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2672 2673 return ops, customStat 2674 2675 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2676 """ 2677 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2678 2679 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2680 Warning! Broker server used ISO UTC time by default. 2681 2682 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2683 Also, `historyFile` used to update history with `onlyMissing` parameter. 2684 2685 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2686 2687 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2688 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2689 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2690 `"hour"`, `"day"`. Default: `"hour"`. 2691 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2692 False by default. Warning! History appends only from last candle to current time 2693 with always update last candle! 2694 :param csvSep: separator if csv-file is used, `,` by default. 2695 :param show: if `True` then also prints Pandas DataFrame to the console. 2696 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2697 `["date", "time", "open", "high", "low", "close", "volume"]`. 2698 """ 2699 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2700 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2701 history = None # empty pandas object for history 2702 2703 if interval not in TKS_CANDLE_INTERVALS.keys(): 2704 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2705 raise Exception("Incorrect value") 2706 2707 if not (self._ticker or self._figi): 2708 uLogger.error("Ticker or FIGI must be defined!") 2709 raise Exception("Ticker or FIGI required") 2710 2711 if self._ticker and not self._figi: 2712 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2713 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2714 2715 if self._figi and not self._ticker: 2716 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2717 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2718 2719 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2720 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2721 if interval.lower() != "day": 2722 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2723 2724 delta = dtEnd - dtStart # current UTC time minus last time in file 2725 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2726 2727 # calculate history length in candles: 2728 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2729 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2730 length += 1 # to avoid fraction time 2731 2732 # calculate data blocks count: 2733 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2734 2735 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2736 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2737 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2738 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2739 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2740 2741 tempOld = None # pandas object for old history, if --only-missing key present 2742 lastTime = None # datetime object of last old candle in file 2743 2744 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2745 uLogger.debug("--only-missing key present, add only last missing candles...") 2746 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2747 2748 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2749 2750 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2751 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2752 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2753 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2754 2755 # get last datetime object from last string in file or minus 1 delta if file is empty: 2756 if len(tempOld) > 0: 2757 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2758 2759 else: 2760 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2761 2762 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2763 2764 responseJSONs = [] # raw history blocks of data 2765 2766 blockEnd = dtEnd 2767 for item in range(blocks): 2768 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2769 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2770 2771 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2772 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2773 )) 2774 2775 if blockStart == blockEnd: 2776 uLogger.debug("Skipped this zero-length block...") 2777 2778 else: 2779 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2780 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2781 self.body = str({ 2782 "figi": self._figi, 2783 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2784 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2785 "interval": TKS_CANDLE_INTERVALS[interval][0] 2786 }) 2787 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2788 2789 if "code" in responseJSON.keys(): 2790 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2791 2792 else: 2793 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2794 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2795 2796 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2797 2798 blockEnd = blockStart 2799 2800 printCount = len(responseJSONs) # candles to show in console 2801 if responseJSONs: 2802 tempHistory = pd.DataFrame( 2803 data={ 2804 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2805 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2806 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2807 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2808 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2809 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2810 "volume": [int(item["volume"]) for item in responseJSONs], 2811 }, 2812 index=range(len(responseJSONs)), 2813 columns=["date", "time", "open", "high", "low", "close", "volume"], 2814 ) 2815 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2816 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2817 2818 # append only newest candles to old history if --only-missing key present: 2819 if onlyMissing and tempOld is not None and lastTime is not None: 2820 index = 0 # find start index in tempHistory data: 2821 2822 for i, item in tempHistory.iterrows(): 2823 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2824 2825 if curTime == lastTime: 2826 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2827 index = i 2828 printCount = index + 1 2829 break 2830 2831 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2832 2833 else: 2834 history = tempHistory # if no `--only-missing` key then load full data from server 2835 2836 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2837 2838 if history is not None and not history.empty: 2839 if show: 2840 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2841 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2842 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2843 )) 2844 2845 else: 2846 uLogger.warning("Received an empty candles history!") 2847 2848 if self.historyFile is not None: 2849 if history is not None and not history.empty: 2850 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2851 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2852 2853 else: 2854 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2855 2856 else: 2857 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2858 2859 return history 2860 2861 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2862 """ 2863 Load candles history from csv-file and return Pandas DataFrame object. 2864 2865 See also: `History()` and `ShowHistoryChart()` methods. 2866 2867 :param filePath: path to csv-file to open. 2868 """ 2869 loadedHistory = None # init candles data object 2870 2871 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2872 2873 if os.path.exists(filePath): 2874 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2875 2876 tfStr = self.priceModel.FormattedDelta( 2877 self.priceModel.timeframe, 2878 "{days} days {hours}h {minutes}m {seconds}s", 2879 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2880 self.priceModel.timeframe, 2881 "{hours}h {minutes}m {seconds}s", 2882 ) 2883 2884 if loadedHistory is not None and not loadedHistory.empty: 2885 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2886 len(loadedHistory), 2887 tfStr, 2888 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2889 ) 2890 2891 else: 2892 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2893 2894 else: 2895 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2896 2897 return loadedHistory 2898 2899 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2900 """ 2901 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2902 2903 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2904 Default: `index.html` (both for interact and non-interact candlesticks chart). 2905 2906 See also: `History()` and `LoadHistory()` methods. 2907 2908 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2909 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2910 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2911 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2912 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2913 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2914 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2915 """ 2916 if isinstance(candles, str): 2917 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2918 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2919 2920 elif isinstance(candles, pd.DataFrame): 2921 self.priceModel.prices = candles # set candles chain from variable 2922 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2923 2924 if "datetime" not in candles.columns: 2925 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2926 2927 else: 2928 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2929 raise Exception("Incorrect value") 2930 2931 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2932 2933 if interact: 2934 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2935 2936 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2937 2938 else: 2939 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2940 2941 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2942 2943 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2944 2945 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2946 """ 2947 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2948 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2949 2950 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2951 2952 :param operation: string "Buy" or "Sell". 2953 :param lots: volume, integer count of lots >= 1. 2954 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2955 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2956 :param expDate: string "Undefined" by default or local date in future, 2957 it is a string with format `%Y-%m-%d %H:%M:%S`. 2958 :return: JSON with response from broker server. 2959 """ 2960 if self.accountId is None or not self.accountId: 2961 uLogger.error("Variable `accountId` must be defined for using this method!") 2962 raise Exception("Account ID required") 2963 2964 if operation is None or not operation or operation not in ("Buy", "Sell"): 2965 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2966 raise Exception("Incorrect value") 2967 2968 if lots is None or lots < 1: 2969 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2970 lots = 1 2971 2972 if tp is None or tp < 0: 2973 tp = 0 2974 2975 if sl is None or sl < 0: 2976 sl = 0 2977 2978 if expDate is None or not expDate: 2979 expDate = "Undefined" 2980 2981 if not (self._ticker or self._figi): 2982 uLogger.error("Ticker or FIGI must be defined!") 2983 raise Exception("Ticker or FIGI required") 2984 2985 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2986 self._ticker = instrument["ticker"] 2987 self._figi = instrument["figi"] 2988 2989 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 2990 2991 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2992 self.body = str({ 2993 "figi": self._figi, 2994 "quantity": str(lots), 2995 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2996 "accountId": str(self.accountId), 2997 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2998 }) 2999 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3000 3001 if "orderId" in response.keys(): 3002 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3003 operation, response["orderId"], 3004 self._ticker, self._figi, lots, 3005 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3006 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3007 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3008 )) 3009 3010 if tp > 0: 3011 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3012 3013 if sl > 0: 3014 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3015 3016 else: 3017 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3018 3019 return response 3020 3021 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3022 """ 3023 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3024 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3025 3026 See also: `Order()` and `Trade()` docstrings. 3027 3028 :param lots: volume, integer count of lots >= 1. 3029 :param tp: float > 0, take profit price of stop-order. 3030 :param sl: float > 0, stop loss price of stop-order. 3031 :param expDate: it's a local date in future. 3032 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3033 :return: JSON with response from broker server. 3034 """ 3035 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3036 3037 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3038 """ 3039 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3040 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3041 3042 See also: `Order()` and `Trade()` docstrings. 3043 3044 :param lots: volume, integer count of lots >= 1. 3045 :param tp: float > 0, take profit price of stop-order. 3046 :param sl: float > 0, stop loss price of stop-order. 3047 :param expDate: it's a local date in the future. 3048 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3049 :return: JSON with response from broker server. 3050 """ 3051 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3052 3053 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3054 """ 3055 Close position of given instruments. 3056 3057 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3058 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3059 This avoids unnecessary downloading data from the server. 3060 """ 3061 if instruments is None or not instruments: 3062 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3063 raise Exception("Ticker or FIGI required") 3064 3065 if isinstance(instruments, str): 3066 instruments = [instruments] 3067 3068 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3069 if uniqueInstruments: 3070 if portfolio is None or not portfolio: 3071 portfolio = self.Overview(show=False) 3072 3073 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3074 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3075 3076 for self._figi in uniqueInstruments: 3077 if self._figi not in allOpened: 3078 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3079 continue 3080 3081 # search open trade info about instrument by ticker: 3082 instrument = {} 3083 for iType in TKS_INSTRUMENTS: 3084 if instrument: 3085 break 3086 3087 for item in portfolio["stat"][iType]: 3088 if item["figi"] == self._figi: 3089 instrument = item 3090 break 3091 3092 if instrument: 3093 self._ticker = instrument["ticker"] 3094 self._figi = instrument["figi"] 3095 3096 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3097 self._ticker, 3098 self._figi, 3099 int(instrument["volume"]), 3100 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3101 )) 3102 3103 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3104 3105 if tradeLots > 0: 3106 if instrument["blocked"] > 0: 3107 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3108 instrument["blocked"], 3109 self._ticker, 3110 tradeLots, 3111 )) 3112 3113 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3114 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3115 3116 else: 3117 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3118 3119 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3120 """ 3121 Close all positions of given instruments with defined type. 3122 3123 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3124 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3125 This avoids unnecessary downloading data from the server. 3126 """ 3127 if iType not in TKS_INSTRUMENTS: 3128 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3129 3130 else: 3131 if portfolio is None or not portfolio: 3132 portfolio = self.Overview(show=False) 3133 3134 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3135 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3136 3137 if tickers and portfolio: 3138 self.CloseTrades(tickers, portfolio) 3139 3140 else: 3141 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3142 3143 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3144 """ 3145 Universal method to create market or limit orders with all available parameters for current `accountId`. 3146 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3147 3148 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3149 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3150 3151 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3152 then broker immediately open market order as you can do simple --buy or --sell operations! 3153 3154 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3155 When current price will go up or down to target price value then broker opens a limit order. 3156 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3157 3158 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3159 3160 :param operation: string "Buy" or "Sell". 3161 :param orderType: string "Limit" or "Stop". 3162 :param lots: volume, integer count of lots >= 1. 3163 :param targetPrice: target price > 0. This is open trade price for limit order. 3164 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3165 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3166 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3167 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3168 Stop loss order always executed by market price. 3169 :param expDate: string "Undefined" by default or local date in future. 3170 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3171 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3172 A limit order has no expiration date, it lasts until the end of the trading day. 3173 :return: JSON with response from broker server. 3174 """ 3175 if self.accountId is None or not self.accountId: 3176 uLogger.error("Variable `accountId` must be defined for using this method!") 3177 raise Exception("Account ID required") 3178 3179 if operation is None or not operation or operation not in ("Buy", "Sell"): 3180 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3181 raise Exception("Incorrect value") 3182 3183 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3184 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3185 raise Exception("Incorrect value") 3186 3187 if lots is None or lots < 1: 3188 uLogger.error("You must define trade volume > 0: integer count of lots!") 3189 raise Exception("Incorrect value") 3190 3191 if targetPrice is None or targetPrice <= 0: 3192 uLogger.error("Target price for limit-order must be greater than 0!") 3193 raise Exception("Incorrect value") 3194 3195 if limitPrice is None or limitPrice <= 0: 3196 limitPrice = targetPrice 3197 3198 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3199 stopType = "Limit" 3200 3201 if expDate is None or not expDate: 3202 expDate = "Undefined" 3203 3204 if not (self._ticker or self._figi): 3205 uLogger.error("Tocker or FIGI must be defined!") 3206 raise Exception("Ticker or FIGI required") 3207 3208 response = {} 3209 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3210 self._ticker = instrument["ticker"] 3211 self._figi = instrument["figi"] 3212 3213 if orderType == "Limit": 3214 uLogger.debug( 3215 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3216 self._ticker, self._figi, 3217 operation, lots, targetPrice, instrument["currency"], 3218 )) 3219 3220 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3221 self.body = str({ 3222 "figi": self._figi, 3223 "quantity": str(lots), 3224 "price": FloatToNano(targetPrice), 3225 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3226 "accountId": str(self.accountId), 3227 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3228 }) 3229 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3230 3231 if "orderId" in response.keys(): 3232 uLogger.info( 3233 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3234 response["orderId"], self._ticker, self._figi, operation, lots, 3235 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3236 )) 3237 3238 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3239 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3240 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3241 targetPrice, instrument["currency"], 3242 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3243 )) 3244 3245 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3246 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3247 targetPrice, instrument["currency"], 3248 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3249 )) 3250 3251 else: 3252 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3253 3254 if orderType == "Stop": 3255 uLogger.debug( 3256 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3257 self._ticker, self._figi, 3258 operation, lots, 3259 targetPrice, instrument["currency"], 3260 limitPrice, instrument["currency"], 3261 stopType, expDate, 3262 )) 3263 3264 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3265 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3266 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3267 3268 body = { 3269 "figi": self._figi, 3270 "quantity": str(lots), 3271 "price": FloatToNano(limitPrice), 3272 "stopPrice": FloatToNano(targetPrice), 3273 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3274 "accountId": str(self.accountId), 3275 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3276 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3277 } 3278 3279 if expDateUTC: 3280 body["expireDate"] = expDateUTC 3281 3282 self.body = str(body) 3283 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3284 3285 if "stopOrderId" in response.keys(): 3286 uLogger.info( 3287 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3288 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3289 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3290 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3291 TKS_STOP_ORDER_TYPES[stopOrderType], 3292 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3293 )) 3294 3295 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3296 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3297 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3298 targetPrice, instrument["currency"], 3299 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3300 )) 3301 3302 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3303 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3304 targetPrice, instrument["currency"], 3305 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3306 )) 3307 3308 else: 3309 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3310 3311 return response 3312 3313 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3314 """ 3315 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3316 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3317 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3318 See also: `Order()` docstring. 3319 3320 :param lots: volume, integer count of lots >= 1. 3321 :param targetPrice: target price > 0. This is open trade price for limit order. 3322 :return: JSON with response from broker server. 3323 """ 3324 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3325 3326 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3327 """ 3328 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3329 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3330 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3331 target price value then broker opens a limit order. See also: `Order()` docstring. 3332 3333 :param lots: volume, integer count of lots >= 1. 3334 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3335 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3336 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3337 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3338 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3339 :param expDate: string "Undefined" by default or local date in future. 3340 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3341 This date is converting to UTC format for server. 3342 :return: JSON with response from broker server. 3343 """ 3344 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3345 3346 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3347 """ 3348 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3349 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3350 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3351 See also: `Order()` docstring. 3352 3353 :param lots: volume, integer count of lots >= 1. 3354 :param targetPrice: target price > 0. This is open trade price for limit order. 3355 :return: JSON with response from broker server. 3356 """ 3357 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3358 3359 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3360 """ 3361 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3362 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3363 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3364 target price value then broker opens a limit order. See also: `Order()` docstring. 3365 3366 :param lots: volume, integer count of lots >= 1. 3367 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3368 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3369 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3370 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3371 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3372 :param expDate: string "Undefined" by default or local date in future. 3373 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3374 This date is converting to UTC format for server. 3375 :return: JSON with response from broker server. 3376 """ 3377 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3378 3379 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3380 """ 3381 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3382 3383 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3384 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3385 This avoids unnecessary downloading data from the server. 3386 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3387 """ 3388 if self.accountId is None or not self.accountId: 3389 uLogger.error("Variable `accountId` must be defined for using this method!") 3390 raise Exception("Account ID required") 3391 3392 if orderIDs: 3393 if allOrdersIDs is None: 3394 rawOrders = self.RequestPendingOrders() 3395 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3396 3397 if allStopOrdersIDs is None: 3398 rawStopOrders = self.RequestStopOrders() 3399 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3400 3401 for orderID in orderIDs: 3402 idInPendingOrders = orderID in allOrdersIDs 3403 idInStopOrders = orderID in allStopOrdersIDs 3404 3405 if not (idInPendingOrders or idInStopOrders): 3406 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3407 continue 3408 3409 else: 3410 if idInPendingOrders: 3411 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3412 3413 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3414 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3415 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3416 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3417 3418 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3419 if self.moreDebug: 3420 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3421 3422 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3423 3424 else: 3425 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3426 3427 elif idInStopOrders: 3428 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3429 3430 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3431 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3432 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3433 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3434 3435 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3436 if self.moreDebug: 3437 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3438 3439 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3440 3441 else: 3442 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3443 3444 else: 3445 continue 3446 3447 def CloseAllOrders(self) -> None: 3448 """ 3449 Gets a list of open pending and stop orders and cancel it all. 3450 """ 3451 rawOrders = self.RequestPendingOrders() 3452 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3453 lenOrders = len(allOrdersIDs) 3454 3455 rawStopOrders = self.RequestStopOrders() 3456 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3457 lenSOrders = len(allStopOrdersIDs) 3458 3459 if lenOrders > 0 or lenSOrders > 0: 3460 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3461 3462 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3463 3464 else: 3465 uLogger.info("Orders not found, nothing to cancel.") 3466 3467 def CloseAll(self, *args) -> None: 3468 """ 3469 Close all available (not blocked) opened trades and orders. 3470 3471 Also, you can select one or more keywords case-insensitive: 3472 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3473 3474 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3475 """ 3476 overview = self.Overview(show=False) # get all open trades info 3477 3478 if len(args) == 0: 3479 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3480 self.CloseAllOrders() # close all pending and stop orders 3481 3482 for iType in TKS_INSTRUMENTS: 3483 if iType != "Currencies": 3484 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3485 3486 else: 3487 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3488 lowerArgs = [x.lower() for x in args] 3489 3490 if "orders" in lowerArgs: 3491 self.CloseAllOrders() # close all pending and stop orders 3492 3493 for iType in TKS_INSTRUMENTS: 3494 if iType.lower() in lowerArgs and iType != "Currencies": 3495 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3496 3497 def CloseAllByTicker(self, instrument: str) -> None: 3498 """ 3499 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3500 3501 This method searches opened trade and orders of instrument throw all portfolio and then use 3502 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3503 3504 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3505 3506 :param instrument: string with ticker. 3507 """ 3508 if instrument is None or not instrument: 3509 uLogger.error("Ticker name must be defined for using this method!") 3510 raise Exception("Ticker required") 3511 3512 overview = self.Overview(show=False) # get user portfolio with all open trades info 3513 3514 self._ticker = instrument # try to set instrument as ticker 3515 self._figi = "" 3516 3517 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3518 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3519 3520 if limitAll and self.IsInLimitOrders(portfolio=overview): 3521 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3522 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3523 3524 if stopAll and self.IsInStopOrders(portfolio=overview): 3525 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3526 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3527 3528 if self.IsInPortfolio(portfolio=overview): 3529 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3530 self.CloseTrades(instruments=[instrument], portfolio=overview) 3531 3532 def CloseAllByFIGI(self, instrument: str) -> None: 3533 """ 3534 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3535 3536 This method searches opened trade and orders of instrument throw all portfolio and then use 3537 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3538 3539 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3540 3541 :param instrument: string with FIGI id. 3542 """ 3543 if instrument is None or not instrument: 3544 uLogger.error("FIGI id must be defined for using this method!") 3545 raise Exception("FIGI required") 3546 3547 overview = self.Overview(show=False) # get user portfolio with all open trades info 3548 3549 self._ticker = "" 3550 self._figi = instrument # try to set instrument as FIGI id 3551 3552 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3553 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3554 3555 if limitAll and self.IsInLimitOrders(portfolio=overview): 3556 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3557 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3558 3559 if stopAll and self.IsInStopOrders(portfolio=overview): 3560 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3561 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3562 3563 if self.IsInPortfolio(portfolio=overview): 3564 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3565 self.CloseTrades(instruments=[instrument], portfolio=overview) 3566 3567 @staticmethod 3568 def ParseOrderParameters(operation, **inputParameters): 3569 """ 3570 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3571 3572 :param operation: string "Buy" or "Sell". 3573 :param inputParameters: this is dict of strings that looks like this 3574 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3575 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3576 "prices" key: one or more prices to open limit-orders 3577 Counts of values in lots and prices lists must be equals! 3578 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3579 """ 3580 # TODO: update order grid work with api v2 3581 pass 3582 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3583 # 3584 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3585 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3586 # raise Exception("Incorrect value") 3587 # 3588 # if "l" in inputParameters.keys(): 3589 # inputParameters["lots"] = inputParameters.pop("l") 3590 # 3591 # if "p" in inputParameters.keys(): 3592 # inputParameters["prices"] = inputParameters.pop("p") 3593 # 3594 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3595 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3596 # raise Exception("Incorrect value") 3597 # 3598 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3599 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3600 # 3601 # if len(lots) != len(prices): 3602 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3603 # raise Exception("Incorrect value") 3604 # 3605 # uLogger.debug("Extracted parameters for orders:") 3606 # uLogger.debug("lots = {}".format(lots)) 3607 # uLogger.debug("prices = {}".format(prices)) 3608 # 3609 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3610 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3611 # uLogger.debug("Order parameters: {}".format(result)) 3612 # 3613 # return result 3614 3615 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3616 """ 3617 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3618 3619 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3620 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3621 """ 3622 result = False 3623 msg = "Instrument not defined!" 3624 3625 if portfolio is None or not portfolio: 3626 portfolio = self.Overview(show=False) 3627 3628 if self._ticker: 3629 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3630 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3631 3632 for iType in TKS_INSTRUMENTS: 3633 for instrument in portfolio["stat"][iType]: 3634 if instrument["ticker"] == self._ticker: 3635 result = True 3636 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3637 break 3638 3639 elif self._figi: 3640 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3641 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3642 3643 for iType in TKS_INSTRUMENTS: 3644 for instrument in portfolio["stat"][iType]: 3645 if instrument["figi"] == self._figi: 3646 result = True 3647 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3648 break 3649 3650 else: 3651 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3652 3653 uLogger.debug(msg) 3654 3655 return result 3656 3657 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3658 """ 3659 Returns instrument from the user's portfolio if it presents there. 3660 Instrument must be defined by `ticker` (highly priority) or `figi`. 3661 3662 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3663 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3664 """ 3665 result = None 3666 msg = "Instrument not defined!" 3667 3668 if portfolio is None or not portfolio: 3669 portfolio = self.Overview(show=False) 3670 3671 if self._ticker: 3672 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3673 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3674 3675 for iType in TKS_INSTRUMENTS: 3676 for instrument in portfolio["stat"][iType]: 3677 if instrument["ticker"] == self._ticker: 3678 result = instrument 3679 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3680 break 3681 3682 elif self._figi: 3683 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3684 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3685 3686 for iType in TKS_INSTRUMENTS: 3687 for instrument in portfolio["stat"][iType]: 3688 if instrument["figi"] == self._figi: 3689 result = instrument 3690 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3691 break 3692 3693 else: 3694 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3695 3696 uLogger.debug(msg) 3697 3698 return result 3699 3700 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3701 """ 3702 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3703 3704 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3705 3706 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3707 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3708 """ 3709 result = False 3710 msg = "Instrument not defined!" 3711 3712 if portfolio is None or not portfolio: 3713 portfolio = self.Overview(show=False) 3714 3715 if self._ticker: 3716 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3717 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3718 3719 for instrument in portfolio["stat"]["orders"]: 3720 if instrument["ticker"] == self._ticker: 3721 result = True 3722 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3723 break 3724 3725 elif self._figi: 3726 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3727 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3728 3729 for instrument in portfolio["stat"]["orders"]: 3730 if instrument["figi"] == self._figi: 3731 result = True 3732 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3733 break 3734 3735 else: 3736 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3737 3738 uLogger.debug(msg) 3739 3740 return result 3741 3742 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3743 """ 3744 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3745 Instrument must be defined by `ticker` (highly priority) or `figi`. 3746 3747 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3748 3749 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3750 :return: list with `orderID`s of limit orders. 3751 """ 3752 result = [] 3753 msg = "Instrument not defined!" 3754 3755 if portfolio is None or not portfolio: 3756 portfolio = self.Overview(show=False) 3757 3758 if self._ticker: 3759 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3760 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3761 3762 for instrument in portfolio["stat"]["orders"]: 3763 if instrument["ticker"] == self._ticker: 3764 result.append(instrument["orderID"]) 3765 3766 if result: 3767 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3768 3769 elif self._figi: 3770 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3771 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3772 3773 for instrument in portfolio["stat"]["orders"]: 3774 if instrument["figi"] == self._figi: 3775 result.append(instrument["orderID"]) 3776 3777 if result: 3778 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3779 3780 else: 3781 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3782 3783 uLogger.debug(msg) 3784 3785 return result 3786 3787 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3788 """ 3789 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3790 3791 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3792 3793 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3794 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3795 """ 3796 result = False 3797 msg = "Instrument not defined!" 3798 3799 if portfolio is None or not portfolio: 3800 portfolio = self.Overview(show=False) 3801 3802 if self._ticker: 3803 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3804 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3805 3806 for instrument in portfolio["stat"]["stopOrders"]: 3807 if instrument["ticker"] == self._ticker: 3808 result = True 3809 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3810 break 3811 3812 elif self._figi: 3813 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3814 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3815 3816 for instrument in portfolio["stat"]["stopOrders"]: 3817 if instrument["figi"] == self._figi: 3818 result = True 3819 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3820 break 3821 3822 else: 3823 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3824 3825 uLogger.debug(msg) 3826 3827 return result 3828 3829 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3830 """ 3831 Returns list with all `orderID`s of opened stop orders for the instrument. 3832 Instrument must be defined by `ticker` (highly priority) or `figi`. 3833 3834 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3835 3836 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3837 :return: list with `orderID`s of stop orders. 3838 """ 3839 result = [] 3840 msg = "Instrument not defined!" 3841 3842 if portfolio is None or not portfolio: 3843 portfolio = self.Overview(show=False) 3844 3845 if self._ticker: 3846 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3847 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3848 3849 for instrument in portfolio["stat"]["stopOrders"]: 3850 if instrument["ticker"] == self._ticker: 3851 result.append(instrument["orderID"]) 3852 3853 if result: 3854 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3855 3856 elif self._figi: 3857 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3858 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3859 3860 for instrument in portfolio["stat"]["stopOrders"]: 3861 if instrument["figi"] == self._figi: 3862 result.append(instrument["orderID"]) 3863 3864 if result: 3865 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3866 3867 else: 3868 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3869 3870 uLogger.debug(msg) 3871 3872 return result 3873 3874 def RequestLimits(self) -> dict: 3875 """ 3876 Method for obtaining the available funds for withdrawal for current `accountId`. 3877 3878 See also: 3879 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3880 - `OverviewLimits()` method 3881 3882 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3883 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3884 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3885 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3886 """ 3887 if self.accountId is None or not self.accountId: 3888 uLogger.error("Variable `accountId` must be defined for using this method!") 3889 raise Exception("Account ID required") 3890 3891 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3892 3893 self.body = str({"accountId": self.accountId}) 3894 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3895 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3896 3897 if self.moreDebug: 3898 uLogger.debug("Records about available funds for withdrawal successfully received") 3899 3900 return rawLimits 3901 3902 def OverviewLimits(self, show: bool = False) -> dict: 3903 """ 3904 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3905 3906 See also: `RequestLimits()`. 3907 3908 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3909 :return: dict with raw parsed data from server and some calculated statistics about it. 3910 """ 3911 if self.accountId is None or not self.accountId: 3912 uLogger.error("Variable `accountId` must be defined for using this method!") 3913 raise Exception("Account ID required") 3914 3915 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3916 3917 view = { 3918 "rawLimits": rawLimits, 3919 "limits": { # parsed data for every currency: 3920 "money": { # this is an array of portfolio currency positions 3921 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3922 }, 3923 "blocked": { # this is an array of blocked currency 3924 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3925 }, 3926 "blockedGuarantee": { # this is locked money under collateral for futures 3927 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3928 }, 3929 }, 3930 } 3931 3932 # --- Prepare text table with limits in human-readable format: 3933 if show: 3934 info = [ 3935 "# Withdrawal limits\n\n", 3936 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3937 "* **Account ID:** [{}]\n".format(self.accountId), 3938 ] 3939 3940 if view["limits"]["money"]: 3941 info.extend([ 3942 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3943 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3944 ]) 3945 3946 else: 3947 info.append("\nNo withdrawal limits\n") 3948 3949 for curr in view["limits"]["money"].keys(): 3950 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3951 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3952 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3953 3954 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3955 "[{}]".format(curr), 3956 "{:.2f}".format(view["limits"]["money"][curr]), 3957 "{:.2f}".format(availableMoney), 3958 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3959 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3960 ) 3961 3962 if curr == "rub": 3963 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3964 3965 else: 3966 info.append(infoStr) 3967 3968 infoText = "".join(info) 3969 3970 uLogger.info(infoText) 3971 3972 if self.withdrawalLimitsFile: 3973 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3974 fH.write(infoText) 3975 3976 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3977 3978 if self.useHTMLReports: 3979 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3980 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3981 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 3982 3983 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 3984 3985 return view 3986 3987 def RequestAccounts(self) -> dict: 3988 """ 3989 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3990 3991 See also: 3992 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3993 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3994 - `OverviewUserInfo()` method 3995 3996 :return: dict with raw data from server that contains accounts info. Example of dict: 3997 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3998 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3999 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4000 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4001 """ 4002 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4003 4004 self.body = str({}) 4005 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4006 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4007 4008 if self.moreDebug: 4009 uLogger.debug("Records about available accounts successfully received") 4010 4011 return rawAccounts 4012 4013 def RequestUserInfo(self) -> dict: 4014 """ 4015 Method for requesting common user's information. 4016 4017 See also: 4018 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4019 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4020 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4021 - `OverviewUserInfo()` method 4022 4023 :return: dict with raw data from server that contains user's information. Example of dict: 4024 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4025 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4026 """ 4027 uLogger.debug("Requesting common user's information. Wait, please...") 4028 4029 self.body = str({}) 4030 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4031 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4032 4033 if self.moreDebug: 4034 uLogger.debug("Records about current user successfully received") 4035 4036 return rawUserInfo 4037 4038 def RequestMarginStatus(self, accountId: str = None) -> dict: 4039 """ 4040 Method for requesting margin calculation for defined account ID. 4041 4042 See also: 4043 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4044 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4045 - `OverviewUserInfo()` method 4046 4047 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4048 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4049 Example of responses: 4050 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4051 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4052 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4053 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4054 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4055 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4056 """ 4057 if accountId is None or not accountId: 4058 if self.accountId is None or not self.accountId: 4059 uLogger.error("Variable `accountId` must be defined for using this method!") 4060 raise Exception("Account ID required") 4061 4062 else: 4063 accountId = self.accountId # use `self.accountId` (main ID) by default 4064 4065 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4066 4067 self.body = str({"accountId": accountId}) 4068 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4069 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4070 4071 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4072 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4073 rawMargin = {} 4074 4075 else: 4076 if self.moreDebug: 4077 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4078 4079 return rawMargin 4080 4081 def RequestTariffLimits(self) -> dict: 4082 """ 4083 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4084 4085 See also: 4086 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4087 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4088 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4089 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4090 - `OverviewUserInfo()` method 4091 4092 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4093 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4094 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4095 """ 4096 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4097 4098 self.body = str({}) 4099 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4100 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4101 4102 if self.moreDebug: 4103 uLogger.debug("Records with limits of current tariff successfully received") 4104 4105 return rawTariffLimits 4106 4107 def RequestBondCoupons(self, iJSON: dict) -> dict: 4108 """ 4109 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4110 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4111 All dates are in UTC timezone. 4112 4113 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4114 Documentation: 4115 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4116 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4117 4118 See also: `ExtendBondsData()`. 4119 4120 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4121 If raw iJSON is not data of bond then server returns an error [400] with message: 4122 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4123 :return: dictionary with bond payment calendar. Response example 4124 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4125 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4126 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4127 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4128 """ 4129 if iJSON["figi"] is None or not iJSON["figi"]: 4130 uLogger.error("FIGI must be defined for using this method!") 4131 raise Exception("FIGI required") 4132 4133 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4134 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4135 4136 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4137 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4138 self._figi, 4139 startDate, 4140 endDate, 4141 )) 4142 4143 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4144 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4145 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4146 4147 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4148 uLogger.warning("Instrument type is not bond!") 4149 4150 else: 4151 if self.moreDebug: 4152 uLogger.debug("Records about bond payment calendar successfully received") 4153 4154 return calendar 4155 4156 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4157 """ 4158 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4159 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4160 coupon yields, current yields and some statistics etc. 4161 4162 WARNING! This is too long operation if a lot of bonds requested from broker server. 4163 4164 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4165 4166 :param instruments: list of strings with tickers or FIGIs. 4167 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4168 for further used by data scientists or stock analytics. 4169 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4170 In XLSX-file and Pandas DataFrame fields mean: 4171 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4172 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4173 """ 4174 if instruments is None or not instruments: 4175 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4176 raise Exception("Ticker or FIGI required") 4177 4178 if isinstance(instruments, str): 4179 instruments = [instruments] 4180 4181 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4182 4183 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4184 4185 iCount = len(uniqueInstruments) 4186 tooLong = iCount >= 20 4187 if tooLong: 4188 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4189 4190 bonds = None 4191 for i, self._figi in enumerate(uniqueInstruments): 4192 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4193 4194 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4195 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4196 rawBond = self.SearchByFIGI(requestPrice=True) 4197 4198 # Widen raw data with UTC current time (iData["actualDateTime"]): 4199 actualDate = datetime.now(tzutc()) 4200 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4201 4202 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4203 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4204 4205 # Replace some values with human-readable: 4206 iData["nominalCurrency"] = iData["nominal"]["currency"] 4207 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4208 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4209 iData["aciCurrency"] = iData["aciValue"]["currency"] 4210 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4211 iData["issueSize"] = int(iData["issueSize"]) 4212 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4213 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4214 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4215 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4216 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4217 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4218 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4219 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4220 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4221 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4222 4223 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4224 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4225 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4226 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4227 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4228 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4229 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4230 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4231 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4232 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4233 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4234 4235 # Widen raw data with calendar data from `rawCalendar` values: 4236 calendarData = [] 4237 if "events" in iData["rawCalendar"].keys(): 4238 for item in iData["rawCalendar"]["events"]: 4239 calendarData.append({ 4240 "couponDate": item["couponDate"], 4241 "couponNumber": int(item["couponNumber"]), 4242 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4243 "payCurrency": item["payOneBond"]["currency"], 4244 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4245 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4246 "couponStartDate": item["couponStartDate"], 4247 "couponEndDate": item["couponEndDate"], 4248 "couponPeriod": item["couponPeriod"], 4249 }) 4250 4251 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4252 if "maturityDate" not in iData.keys(): 4253 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4254 4255 # Widen raw data with Coupon Rate. 4256 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4257 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4258 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4259 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4260 4261 # Widen raw data with Yield to Maturity (YTM) on current date. 4262 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4263 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4264 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4265 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4266 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4267 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4268 4269 iData["calendar"] = calendarData # adds calendar at the end 4270 4271 # Remove not used data: 4272 iData.pop("uid") 4273 iData.pop("positionUid") 4274 iData.pop("currentPrice") 4275 iData.pop("rawCalendar") 4276 4277 colNames = list(iData.keys()) 4278 if bonds is None: 4279 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4280 4281 else: 4282 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4283 4284 else: 4285 uLogger.warning("Instrument is not a bond!") 4286 4287 processed = round(100 * (i + 1) / iCount, 1) 4288 if tooLong and processed % 5 == 0: 4289 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4290 4291 else: 4292 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4293 4294 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4295 4296 # Saving bonds from Pandas DataFrame to XLSX sheet: 4297 if xlsx and self.bondsXLSXFile: 4298 with pd.ExcelWriter( 4299 path=self.bondsXLSXFile, 4300 date_format=TKS_DATE_FORMAT, 4301 datetime_format=TKS_DATE_TIME_FORMAT, 4302 mode="w", 4303 ) as writer: 4304 bonds.to_excel( 4305 writer, 4306 sheet_name="Extended bonds data", 4307 index=True, 4308 encoding="UTF-8", 4309 freeze_panes=(1, 1), 4310 ) # saving as XLSX-file with freeze first row and column as headers 4311 4312 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4313 4314 return bonds 4315 4316 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4317 """ 4318 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4319 4320 WARNING! This is too long operation if a lot of bonds requested from broker server. 4321 4322 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4323 4324 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4325 extended information about bonds: main info, current prices, bond payment calendar, 4326 coupon yields, current yields and some statistics etc. 4327 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4328 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4329 for further used by data scientists or stock analytics. 4330 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4331 """ 4332 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4333 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4334 4335 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4336 4337 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4338 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4339 calendar = None 4340 for bond in extBonds.iterrows(): 4341 for item in bond[1]["calendar"]: 4342 cData = { 4343 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4344 "couponDate": item["couponDate"], 4345 "figi": bond[1]["figi"], 4346 "ticker": bond[1]["ticker"], 4347 "name": bond[1]["name"], 4348 "couponNumber": item["couponNumber"], 4349 "payOneBond": item["payOneBond"], 4350 "payCurrency": item["payCurrency"], 4351 "couponType": item["couponType"], 4352 "couponPeriod": item["couponPeriod"], 4353 "fixDate": item["fixDate"], 4354 "couponStartDate": item["couponStartDate"], 4355 "couponEndDate": item["couponEndDate"], 4356 } 4357 4358 if calendar is None: 4359 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4360 4361 else: 4362 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4363 4364 if calendar is not None: 4365 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4366 4367 # Saving calendar from Pandas DataFrame to XLSX sheet: 4368 if xlsx: 4369 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4370 4371 with pd.ExcelWriter( 4372 path=xlsxCalendarFile, 4373 date_format=TKS_DATE_FORMAT, 4374 datetime_format=TKS_DATE_TIME_FORMAT, 4375 mode="w", 4376 ) as writer: 4377 humanReadable = calendar.copy(deep=True) 4378 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4379 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4380 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4381 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4382 humanReadable.columns = colNames # human-readable column names 4383 4384 humanReadable.to_excel( 4385 writer, 4386 sheet_name="Bond payments calendar", 4387 index=False, 4388 encoding="UTF-8", 4389 freeze_panes=(1, 2), 4390 ) # saving as XLSX-file with freeze first row and column as headers 4391 4392 del humanReadable # release df in memory 4393 4394 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4395 4396 return calendar 4397 4398 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4399 """ 4400 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4401 Also, creates Markdown file with calendar data, `calendar.md` by default. 4402 4403 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4404 4405 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4406 extended information about bonds: main info, current prices, bond payment calendar, 4407 coupon yields, current yields and some statistics etc. 4408 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4409 :param show: if `True` then also printing bonds payment calendar to the console, 4410 otherwise save to file `calendarFile` only. `False` by default. 4411 :return: multilines text in Markdown format with bonds payment calendar as a table. 4412 """ 4413 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4414 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4415 4416 infoText = "# Bond payments calendar\n\n" 4417 4418 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4419 4420 if not (calendar is None or calendar.empty): 4421 splitLine = "| | | | | | | | | |\n" 4422 4423 info = [ 4424 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4425 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4426 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4427 ] 4428 4429 newMonth = False 4430 notOneBond = calendar["figi"].nunique() > 1 4431 for i, bond in enumerate(calendar.iterrows()): 4432 if newMonth and notOneBond: 4433 info.append(splitLine) 4434 4435 info.append( 4436 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4437 " √" if bond[1]["paid"] else " —", 4438 bond[1]["couponDate"].split("T")[0], 4439 bond[1]["figi"], 4440 bond[1]["ticker"], 4441 bond[1]["couponNumber"], 4442 "{} {}".format( 4443 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4444 bond[1]["payCurrency"], 4445 ), 4446 bond[1]["couponType"], 4447 bond[1]["couponPeriod"], 4448 bond[1]["fixDate"].split("T")[0], 4449 ) 4450 ) 4451 4452 if i < len(calendar.values) - 1: 4453 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4454 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4455 newMonth = False if curDate.month == nextDate.month else True 4456 4457 else: 4458 newMonth = False 4459 4460 infoText += "".join(info) 4461 4462 if show: 4463 uLogger.info("{}".format(infoText)) 4464 4465 if self.calendarFile is not None: 4466 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4467 fH.write(infoText) 4468 4469 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4470 4471 if self.useHTMLReports: 4472 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4473 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4474 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4475 4476 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4477 4478 else: 4479 infoText += "No data\n" 4480 4481 return infoText 4482 4483 def OverviewAccounts(self, show: bool = False) -> dict: 4484 """ 4485 Method for parsing and show simple table with all available user accounts. 4486 4487 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4488 4489 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4490 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4491 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4492 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4493 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4494 "closed": "—", "access": "Full access" }, ...}}` 4495 """ 4496 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4497 4498 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4499 accounts = { 4500 item["id"]: { 4501 "type": TKS_ACCOUNT_TYPES[item["type"]], 4502 "name": item["name"], 4503 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4504 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4505 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4506 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4507 } for item in rawAccounts["accounts"] 4508 } 4509 4510 # Raw and parsed data with some fields replaced in "stat" section: 4511 view = { 4512 "rawAccounts": rawAccounts, 4513 "stat": accounts, 4514 } 4515 4516 # --- Prepare simple text table with only accounts data in human-readable format: 4517 if show: 4518 info = [ 4519 "# User accounts\n\n", 4520 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4521 "| Account ID | Type | Status | Name |\n", 4522 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4523 ] 4524 4525 for account in view["stat"].keys(): 4526 info.extend([ 4527 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4528 account, 4529 view["stat"][account]["type"], 4530 view["stat"][account]["status"], 4531 view["stat"][account]["name"], 4532 ) 4533 ]) 4534 4535 infoText = "".join(info) 4536 4537 uLogger.info(infoText) 4538 4539 if self.userAccountsFile: 4540 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4541 fH.write(infoText) 4542 4543 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4544 4545 if self.useHTMLReports: 4546 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4547 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4548 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4549 4550 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4551 4552 return view 4553 4554 def OverviewUserInfo(self, show: bool = False) -> dict: 4555 """ 4556 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4557 4558 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4559 4560 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4561 :return: dict with raw parsed data from server and some calculated statistics about it. 4562 """ 4563 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4564 tmpTicker = self._ticker 4565 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4566 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4567 self._ticker = tmpTicker 4568 4569 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4570 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4571 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4572 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4573 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4574 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4575 4576 # This is dict with parsed common user data: 4577 userInfo = { 4578 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4579 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4580 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4581 "tariff": rawUserInfo["tariff"], 4582 } 4583 4584 # This is an array of dict with parsed margin statuses for every account IDs: 4585 margins = {} 4586 for accountId in accounts.keys(): 4587 if rawMargins[accountId]: 4588 margins[accountId] = { 4589 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4590 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4591 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4592 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4593 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4594 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4595 "missing": missing["volume"], 4596 } 4597 4598 else: 4599 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4600 4601 unary = {} # unary-connection limits 4602 for item in rawTariffLimits["unaryLimits"]: 4603 if item["limitPerMinute"] in unary.keys(): 4604 unary[item["limitPerMinute"]].extend(item["methods"]) 4605 4606 else: 4607 unary[item["limitPerMinute"]] = item["methods"] 4608 4609 stream = {} # stream-connection limits 4610 for item in rawTariffLimits["streamLimits"]: 4611 if item["limit"] in stream.keys(): 4612 stream[item["limit"]].extend(item["streams"]) 4613 4614 else: 4615 stream[item["limit"]] = item["streams"] 4616 4617 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4618 limits = { 4619 "unary": unary, 4620 "stream": stream, 4621 } 4622 4623 # Raw and parsed data as an output result: 4624 view = { 4625 "rawUserInfo": rawUserInfo, 4626 "rawAccounts": rawAccounts, 4627 "rawMargins": rawMargins, 4628 "rawTariffLimits": rawTariffLimits, 4629 "stat": { 4630 "overview": overview, 4631 "userInfo": userInfo, 4632 "accounts": accounts, 4633 "margins": margins, 4634 "limits": limits, 4635 }, 4636 } 4637 4638 # --- Prepare text table with user information in human-readable format: 4639 if show: 4640 info = [ 4641 "# Full user information\n\n", 4642 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4643 "## Common information\n\n", 4644 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4645 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4646 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4647 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4648 "\n## User accounts\n\n", 4649 ] 4650 4651 for account in view["stat"]["accounts"].keys(): 4652 info.extend([ 4653 "### ID: [{}]\n\n".format(account), 4654 "| Parameters | Values |\n", 4655 "|----------------------|--------------------------------------------------------------|\n", 4656 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4657 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4658 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4659 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4660 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4661 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4662 ]) 4663 4664 if margins[account]: 4665 info.extend([ 4666 "| Margin status: | Enabled |\n", 4667 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4668 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4669 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4670 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4671 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4672 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4673 ]) 4674 4675 else: 4676 info.append("| Margin status: | Disabled |\n\n") 4677 4678 info.extend([ 4679 "\n## Current user tariff limits\n", 4680 "\n### See also\n", 4681 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4682 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4683 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4684 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4685 "\n### Unary limits\n", 4686 ]) 4687 4688 if unary: 4689 for key, values in sorted(unary.items()): 4690 info.append("\n* Max requests per minute: {}\n".format(key)) 4691 4692 for value in values: 4693 info.append(" - {}\n".format(value)) 4694 4695 else: 4696 info.append("\nNot available\n") 4697 4698 info.append("\n### Stream limits\n") 4699 4700 if stream: 4701 for key, values in sorted(stream.items()): 4702 info.append("\n* Max stream connections: {}\n".format(key)) 4703 4704 for value in values: 4705 info.append(" - {}\n".format(value)) 4706 4707 else: 4708 info.append("\nNot available\n") 4709 4710 infoText = "".join(info) 4711 4712 uLogger.info(infoText) 4713 4714 if self.userInfoFile: 4715 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4716 fH.write(infoText) 4717 4718 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4719 4720 if self.useHTMLReports: 4721 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4722 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4723 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4724 4725 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4726 4727 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
86 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 87 """ 88 Main class init. 89 90 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 91 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 92 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 93 :param useCache: use default cache file with raw data to use instead of `iList`. 94 True by default. Cache is auto-update if new day has come. 95 If you don't want to use cache and always updates raw data then set `useCache=False`. 96 :param defaultCache: path to default cache file. `dump.json` by default. 97 """ 98 if token is None or not token: 99 try: 100 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 101 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 102 103 except KeyError: 104 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 105 raise Exception("Token required") 106 107 else: 108 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 109 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 110 111 if accountId is None or not accountId: 112 try: 113 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 114 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 115 116 except KeyError: 117 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 118 119 else: 120 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 121 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 122 123 self.version = __version__ # duplicate here used TKSBrokerAPI main version 124 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 125 126 Latest version: https://pypi.org/project/tksbrokerapi/ 127 """ 128 129 self.__lock = Lock() # initialize multiprocessing mutex lock 130 131 self.aliases = TKS_TICKER_ALIASES 132 """Some aliases instead official tickers. 133 134 See also: `TKSEnums.TKS_TICKER_ALIASES` 135 """ 136 137 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 138 139 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 140 141 self._ticker = "" 142 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 143 144 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 145 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 146 147 See also: `SearchByTicker()`, `SearchInstruments()`. 148 """ 149 150 self._figi = "" 151 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 152 153 See also: `SearchByFIGI()`, `SearchInstruments()`. 154 """ 155 156 self.depth = 1 157 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 158 159 See also: `GetCurrentPrices()`. 160 """ 161 162 self.server = r"https://invest-public-api.tinkoff.ru/rest" 163 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 164 165 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 166 """ 167 168 uLogger.debug("Broker API server: {}".format(self.server)) 169 170 self.timeout = 15 171 """Server operations timeout in seconds. Default: `15`. 172 173 See also: `SendAPIRequest()`. 174 """ 175 176 self.headers = { 177 "Content-Type": "application/json", 178 "accept": "application/json", 179 "Authorization": "Bearer {}".format(self.token), 180 "x-app-name": "Tim55667757.TKSBrokerAPI", 181 } 182 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 183 184 See also: `SendAPIRequest()`. 185 """ 186 187 self.body = None 188 """Request body which send to broker server. Default: `None`. 189 190 See also: `SendAPIRequest()`. 191 """ 192 193 self.moreDebug = False 194 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 195 196 self.useHTMLReports = False 197 """ 198 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 199 200 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 201 """ 202 203 self.historyFile = None 204 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 205 206 See also: `History()`. 207 """ 208 209 self.htmlHistoryFile = "index.html" 210 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 211 212 See also: `ShowHistoryChart()`. 213 """ 214 215 self.instrumentsFile = "instruments.md" 216 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 217 218 See also: `ShowInstrumentsInfo()`. 219 """ 220 221 self.searchResultsFile = "search-results.md" 222 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 223 224 See also: `SearchInstruments()`. 225 """ 226 227 self.pricesFile = "prices.md" 228 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 229 230 See also: `GetListOfPrices()`. 231 """ 232 233 self.infoFile = "info.md" 234 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 235 236 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 237 """ 238 239 self.bondsXLSXFile = "ext-bonds.xlsx" 240 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 241 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 242 243 See also: `ExtendBondsData()`. 244 """ 245 246 self.calendarFile = "calendar.md" 247 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 248 249 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 250 251 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 252 """ 253 254 self.overviewFile = "overview.md" 255 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 256 257 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 258 """ 259 260 self.overviewDigestFile = "overview-digest.md" 261 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 262 263 See also: `Overview()` with parameter `details="digest"`. 264 """ 265 266 self.overviewPositionsFile = "overview-positions.md" 267 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 268 269 See also: `Overview()` with parameter `details="positions"`. 270 """ 271 272 self.overviewOrdersFile = "overview-orders.md" 273 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 274 275 See also: `Overview()` with parameter `details="orders"`. 276 """ 277 278 self.overviewAnalyticsFile = "overview-analytics.md" 279 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 280 281 See also: `Overview()` with parameter `details="analytics"`. 282 """ 283 284 self.overviewBondsCalendarFile = "overview-calendar.md" 285 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 286 287 See also: `Overview()` with parameter `details="calendar"`. 288 """ 289 290 self.reportFile = "deals.md" 291 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 292 293 See also: `Deals()`. 294 """ 295 296 self.withdrawalLimitsFile = "limits.md" 297 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 298 299 See also: `OverviewLimits()` and `RequestLimits()`. 300 """ 301 302 self.userInfoFile = "user-info.md" 303 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 304 305 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 306 """ 307 308 self.userAccountsFile = "accounts.md" 309 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 310 311 See also: `OverviewAccounts()`, `RequestAccounts()`. 312 """ 313 314 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 315 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 316 317 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 318 319 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 320 """ 321 322 self.iList = None # init iList for raw instruments data 323 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 324 325 See also: `Listing()`, `DumpInstruments()`. 326 """ 327 328 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 329 if useCache: 330 if os.path.exists(self.iListDumpFile): 331 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 332 curTime = datetime.now(tzutc()) 333 334 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 335 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 336 337 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 338 339 else: 340 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 341 342 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 343 os.path.abspath(self.iListDumpFile), 344 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 345 )) 346 347 else: 348 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 349 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 350 351 else: 352 self.iList = self.Listing() # request new raw instruments data from broker server 353 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 354 355 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 356 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 357 358 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 359 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Enables more debug information in this class, such as net request and response headers in all methods. False by default.
If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.
See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.
See also: Overview() with parameter details="calendar".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.
Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc.
More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.
See also: SearchByFIGI(), SearchInstruments().
413 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 414 """ 415 Send GET or POST request to broker server and receive JSON object. 416 417 self.header: must be defining with dictionary of headers. 418 self.body: if define then used as request body. None by default. 419 self.timeout: global request timeout, 15 seconds by default. 420 :param url: url with REST request. 421 :param reqType: send "GET" or "POST" request. "GET" by default. 422 :param retry: how many times retry after first request if an 5xx server errors occurred. 423 :param pause: sleep time in seconds between retries. 424 :return: response JSON (dictionary) from broker. 425 """ 426 if reqType.upper() not in ("GET", "POST"): 427 uLogger.error("You can define request type: `GET` or `POST`!") 428 raise Exception("Incorrect value") 429 430 if self.moreDebug: 431 uLogger.debug("Request parameters:") 432 uLogger.debug(" - REST API URL: {}".format(url)) 433 uLogger.debug(" - request type: {}".format(reqType)) 434 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 435 uLogger.debug(" - body:\n{}".format(self.body)) 436 437 # fast hack to avoid all operations with some tickers/FIGI 438 responseJSON = {} 439 oK = True 440 for item in self.exclude: 441 if item in url: 442 if self.moreDebug: 443 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 444 445 oK = False 446 break 447 448 if oK: 449 with self.__lock: # acquire the mutex lock 450 counter = 0 451 response = None 452 errMsg = "" 453 454 while not response and counter <= retry: 455 if reqType == "GET": 456 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 457 458 if reqType == "POST": 459 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 460 461 if self.moreDebug: 462 uLogger.debug("Response:") 463 uLogger.debug(" - status code: {}".format(response.status_code)) 464 uLogger.debug(" - reason: {}".format(response.reason)) 465 uLogger.debug(" - body length: {}".format(len(response.text))) 466 uLogger.debug(" - headers:\n{}".format(response.headers)) 467 468 # Server returns some headers: 469 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 470 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 471 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 472 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 473 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 474 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 475 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 476 sleep(rateLimitWait) 477 478 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 479 if 400 <= response.status_code < 500: 480 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 481 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 482 483 if "code" in response.text and "message" in response.text: 484 msgDict = self._ParseJSON(rawData=response.text) 485 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 486 487 counter = retry + 1 # do not retry for 4xx errors 488 489 if 500 <= response.status_code < 600: 490 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 491 uLogger.debug(" - not oK, {}".format(errMsg)) 492 493 if "code" in response.text and "message" in response.text: 494 errMsgDict = self._ParseJSON(rawData=response.text) 495 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 496 497 counter += 1 498 499 if counter <= retry: 500 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 501 sleep(pause) 502 503 responseJSON = self._ParseJSON(rawData=response.text) 504 505 if errMsg: 506 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 507 uLogger.error(" - not oK, {}".format(errMsg)) 508 509 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
542 def Listing(self) -> dict: 543 """ 544 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 545 546 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 547 """ 548 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 549 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 550 551 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 552 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 553 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 554 555 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 556 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 557 poolUpdater.close() # close the thread pool 558 poolUpdater.join() # wait a moment until all data returns from threads 559 560 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 561 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 562 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 563 564 # calculate minimum price increment (step) for all instruments and set up instrument's type: 565 for iType in iList.keys(): 566 for ticker in iList[iType]: 567 iList[iType][ticker]["type"] = iType 568 569 if "minPriceIncrement" in iList[iType][ticker].keys(): 570 iList[iType][ticker]["step"] = NanoToFloat( 571 iList[iType][ticker]["minPriceIncrement"]["units"], 572 iList[iType][ticker]["minPriceIncrement"]["nano"], 573 ) 574 575 else: 576 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 577 578 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
580 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 581 """ 582 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 583 584 See also: `DumpInstruments()`, `Listing()`. 585 586 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 587 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 588 """ 589 if self.iListDumpFile is None or not self.iListDumpFile: 590 uLogger.error("Output name of dump file must be defined!") 591 raise Exception("Filename required") 592 593 if not self.iList or forceUpdate: 594 self.iList = self.Listing() 595 596 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 597 598 # Save as XLSX with separated sheets for every type of instruments: 599 with pd.ExcelWriter( 600 path=xlsxDumpFile, 601 date_format=TKS_DATE_FORMAT, 602 datetime_format=TKS_DATE_TIME_FORMAT, 603 mode="w", 604 ) as writer: 605 for iType in TKS_INSTRUMENTS: 606 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 607 df = df[sorted(df)] # sorted by column names 608 df = df.applymap( 609 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 610 na_action="ignore", 611 ) # converting numbers from nano-type to float in every cell 612 df.to_excel( 613 writer, 614 sheet_name=iType, 615 encoding="UTF-8", 616 freeze_panes=(1, 1), 617 ) # saving as XLSX-file with freeze first row and column as headers 618 619 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
621 def DumpInstruments(self, forceUpdate: bool = True) -> str: 622 """ 623 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 624 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 625 626 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 627 628 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 629 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 630 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 631 """ 632 if self.iListDumpFile is None or not self.iListDumpFile: 633 uLogger.error("Output name of dump file must be defined!") 634 raise Exception("Filename required") 635 636 if not self.iList or forceUpdate: 637 self.iList = self.Listing() 638 639 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 640 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 641 fH.write(jsonDump) 642 643 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 644 645 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
647 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 648 """ 649 Show information about one instrument defined by json data and prints it in Markdown format. 650 651 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 652 653 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 654 :param show: if `True` then also printing information about instrument and its current price. 655 :return: multilines text in Markdown format with information about one instrument. 656 """ 657 splitLine = "| | |\n" 658 infoText = "" 659 660 if iJSON is not None and iJSON and isinstance(iJSON, dict): 661 info = [ 662 "# Main information\n\n", 663 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 664 "| Parameters | Values |\n", 665 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 666 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 667 "| Full name: | {:<54} |\n".format(iJSON["name"]), 668 ] 669 670 if "sector" in iJSON.keys() and iJSON["sector"]: 671 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 672 673 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 674 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 675 676 info.extend([ 677 splitLine, 678 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 679 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 680 ]) 681 682 if "isin" in iJSON.keys() and iJSON["isin"]: 683 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 684 685 if "classCode" in iJSON.keys(): 686 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 687 688 info.extend([ 689 splitLine, 690 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 691 splitLine, 692 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 693 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 694 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 695 ]) 696 697 if iJSON["figi"]: 698 self._figi = iJSON["figi"] 699 iJSON = iJSON | self.RequestTradingStatus() 700 701 info.extend([ 702 splitLine, 703 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 704 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 705 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 706 ]) 707 708 info.append(splitLine) 709 710 if "type" in iJSON.keys() and iJSON["type"]: 711 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 712 713 if "shareType" in iJSON.keys() and iJSON["shareType"]: 714 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 715 716 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 717 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 718 719 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 720 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 721 722 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 723 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 724 725 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 726 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 727 728 if "focusType" in iJSON.keys() and iJSON["focusType"]: 729 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 730 731 if "assetType" in iJSON.keys() and iJSON["assetType"]: 732 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 733 734 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 735 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 736 737 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 738 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 739 740 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 741 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 742 743 if "currency" in iJSON.keys(): 744 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 745 746 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 747 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 748 749 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 750 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 751 752 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 753 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 754 755 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 756 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 757 758 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 759 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 760 761 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 762 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 763 764 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 765 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 766 767 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 768 info.append("| Perpetual bond: | Yes |\n") 769 770 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 771 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 772 773 iExt = None 774 if iJSON["type"] == "Bonds": 775 info.extend([ 776 splitLine, 777 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 778 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 779 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 780 iJSON["nominal"]["currency"], 781 )), 782 ]) 783 784 if "floatingCouponFlag" in iJSON.keys(): 785 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 786 787 if "amortizationFlag" in iJSON.keys(): 788 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 789 790 info.append(splitLine) 791 792 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 793 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 794 795 if iJSON["figi"]: 796 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 797 798 info.extend([ 799 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 800 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 801 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 802 ]) 803 804 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 805 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 806 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 807 iJSON["aciValue"]["currency"] 808 ))) 809 810 if "currentPrice" in iJSON.keys(): 811 info.append(splitLine) 812 813 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 814 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 815 816 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 817 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 818 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 819 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 820 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 821 822 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 823 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 824 825 info.extend([ 826 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 827 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 828 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 829 )), 830 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 831 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 832 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 833 )), 834 "| Changes between last deal price and last close | {:<54} |\n".format( 835 "{:.2f}%{}".format( 836 iJSON["currentPrice"]["changes"], 837 " ({}{:.2f} {})".format( 838 "+" if bondChangesDelta > 0 else "", 839 bondChangesDelta, 840 aciCurrency 841 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 842 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 843 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 844 currency 845 ), 846 ) 847 ), 848 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 849 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 850 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 851 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 852 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 853 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 854 )), 855 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 856 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 857 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 858 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 859 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 860 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 861 )), 862 ]) 863 864 if "lot" in iJSON.keys(): 865 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 866 867 if "step" in iJSON.keys() and iJSON["step"] != 0: 868 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 869 870 # Add bond payment calendar: 871 if iJSON["type"] == "Bonds": 872 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 873 info.extend(["\n#", strCalendar]) 874 875 infoText += "".join(info) 876 877 if show: 878 uLogger.info("{}".format(infoText)) 879 880 else: 881 uLogger.debug("{}".format(infoText)) 882 883 if self.infoFile is not None: 884 with open(self.infoFile, "w", encoding="UTF-8") as fH: 885 fH.write(infoText) 886 887 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 888 889 if self.useHTMLReports: 890 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 891 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 892 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 893 894 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 895 896 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self._ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
898 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 899 """ 900 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 901 902 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 903 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 904 :return: JSON formatted data with information about instrument. 905 """ 906 tickerJSON = {} 907 if self.moreDebug: 908 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 909 910 if not self._ticker: 911 uLogger.warning("self._ticker variable is not be empty!") 912 913 else: 914 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 915 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 916 raise Exception("Instrument not allowed") 917 918 if not self.iList: 919 self.iList = self.Listing() 920 921 if self._ticker in self.iList["Shares"].keys(): 922 tickerJSON = self.iList["Shares"][self._ticker] 923 if self.moreDebug: 924 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 925 926 elif self._ticker in self.iList["Currencies"].keys(): 927 tickerJSON = self.iList["Currencies"][self._ticker] 928 if self.moreDebug: 929 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 930 931 elif self._ticker in self.iList["Bonds"].keys(): 932 tickerJSON = self.iList["Bonds"][self._ticker] 933 if self.moreDebug: 934 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 935 936 elif self._ticker in self.iList["Etfs"].keys(): 937 tickerJSON = self.iList["Etfs"][self._ticker] 938 if self.moreDebug: 939 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 940 941 elif self._ticker in self.iList["Futures"].keys(): 942 tickerJSON = self.iList["Futures"][self._ticker] 943 if self.moreDebug: 944 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 945 946 if tickerJSON: 947 self._figi = tickerJSON["figi"] 948 949 if requestPrice: 950 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 951 952 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 953 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 954 955 else: 956 tickerJSON["currentPrice"]["changes"] = 0 957 958 if show: 959 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 960 961 else: 962 if show: 963 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 964 965 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
967 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 968 """ 969 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 970 971 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 972 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 973 :return: JSON formatted data with information about instrument. 974 """ 975 figiJSON = {} 976 if self.moreDebug: 977 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 978 979 if not self._figi: 980 uLogger.warning("self._figi variable is not be empty!") 981 982 else: 983 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 984 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 985 raise Exception("Instrument not allowed") 986 987 if not self.iList: 988 self.iList = self.Listing() 989 990 for item in self.iList["Shares"].keys(): 991 if self._figi == self.iList["Shares"][item]["figi"]: 992 figiJSON = self.iList["Shares"][item] 993 994 if self.moreDebug: 995 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 996 997 break 998 999 if not figiJSON: 1000 for item in self.iList["Currencies"].keys(): 1001 if self._figi == self.iList["Currencies"][item]["figi"]: 1002 figiJSON = self.iList["Currencies"][item] 1003 1004 if self.moreDebug: 1005 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1006 1007 break 1008 1009 if not figiJSON: 1010 for item in self.iList["Bonds"].keys(): 1011 if self._figi == self.iList["Bonds"][item]["figi"]: 1012 figiJSON = self.iList["Bonds"][item] 1013 1014 if self.moreDebug: 1015 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1016 1017 break 1018 1019 if not figiJSON: 1020 for item in self.iList["Etfs"].keys(): 1021 if self._figi == self.iList["Etfs"][item]["figi"]: 1022 figiJSON = self.iList["Etfs"][item] 1023 1024 if self.moreDebug: 1025 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1026 1027 break 1028 1029 if not figiJSON: 1030 for item in self.iList["Futures"].keys(): 1031 if self._figi == self.iList["Futures"][item]["figi"]: 1032 figiJSON = self.iList["Futures"][item] 1033 1034 if self.moreDebug: 1035 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1036 1037 break 1038 1039 if figiJSON: 1040 self._figi = figiJSON["figi"] 1041 self._ticker = figiJSON["ticker"] 1042 1043 if requestPrice: 1044 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1045 1046 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1047 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1048 1049 else: 1050 figiJSON["currentPrice"]["changes"] = 0 1051 1052 if show: 1053 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1054 1055 else: 1056 if show: 1057 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1058 1059 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1061 def GetCurrentPrices(self, show: bool = True) -> dict: 1062 """ 1063 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1064 `{"buy": [{"price": 1243.8, "quantity": 193}, 1065 {"price": 1244.0, "quantity": 168}, 1066 {"price": 1244.8, "quantity": 5}, 1067 {"price": 1245.0, "quantity": 61}, 1068 {"price": 1245.4, "quantity": 60}], 1069 "sell": [{"price": 1243.6, "quantity": 8}, 1070 {"price": 1242.6, "quantity": 10}, 1071 {"price": 1242.4, "quantity": 18}, 1072 {"price": 1242.2, "quantity": 50}, 1073 {"price": 1242.0, "quantity": 113}], 1074 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1075 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1076 - sell: list of dicts with Buyers prices, 1077 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1078 - quantity: volume value by current price in lots, 1079 - limitUp: current trade session limit price, maximum, 1080 - limitDown: current trade session limit price, minimum, 1081 - lastPrice: last deal price of the instrument, 1082 - closePrice: previous trade session close price of the instrument. 1083 1084 See also: `SearchByTicker()` and `SearchByFIGI()`. 1085 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1086 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1087 1088 :param show: if `True` then print DOM to log and console. 1089 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1090 If an error occurred then returns an empty record: 1091 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1092 """ 1093 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1094 1095 if self.depth < 1: 1096 uLogger.error("Depth of Market (DOM) must be >=1!") 1097 raise Exception("Incorrect value") 1098 1099 if not (self._ticker or self._figi): 1100 uLogger.error("self._ticker or self._figi variables must be defined!") 1101 raise Exception("Ticker or FIGI required") 1102 1103 if self._ticker and not self._figi: 1104 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1105 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1106 1107 if not self._ticker and self._figi: 1108 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1109 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1110 1111 if not self._figi: 1112 uLogger.error("FIGI is not defined!") 1113 raise Exception("Ticker or FIGI required") 1114 1115 else: 1116 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1117 1118 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1119 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1120 self.body = str({"figi": self._figi, "depth": self.depth}) 1121 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1122 1123 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1124 # list of dicts with sellers orders: 1125 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1126 1127 # list of dicts with buyers orders: 1128 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1129 1130 # max price of instrument at this time: 1131 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1132 1133 # min price of instrument at this time: 1134 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1135 1136 # last price of deal with instrument: 1137 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1138 1139 # last close price of instrument: 1140 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1141 1142 else: 1143 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1144 uLogger.debug("Server response: {}".format(pricesResponse)) 1145 1146 if show: 1147 if prices["buy"] or prices["sell"]: 1148 info = [ 1149 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1150 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1151 self._ticker, 1152 self._figi, 1153 self.depth, 1154 ), 1155 "-" * 60, "\n", 1156 " Orders of Buyers | Orders of Sellers\n", 1157 "-" * 60, "\n", 1158 " Sell prices (volumes) | Buy prices (volumes)\n", 1159 "-" * 60, "\n", 1160 ] 1161 1162 if not prices["buy"]: 1163 info.append(" | No orders!\n") 1164 sumBuy = 0 1165 1166 else: 1167 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1168 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1169 for item in maxMinSorted: 1170 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1171 1172 if not prices["sell"]: 1173 info.append("No orders! |\n") 1174 sumSell = 0 1175 1176 else: 1177 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1178 for item in prices["sell"]: 1179 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1180 1181 info.extend([ 1182 "-" * 60, "\n", 1183 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1184 "-" * 60, "\n", 1185 ]) 1186 1187 infoText = "".join(info) 1188 1189 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1190 1191 else: 1192 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1193 1194 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1196 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1197 """ 1198 This method get and show information about all available broker instruments for current user account. 1199 If `instrumentsFile` string is not empty then also save information to this file. 1200 1201 :param show: if `True` then print results to console, if `False` — print only to file. 1202 :return: multi-lines string with all available broker instruments 1203 """ 1204 if not self.iList: 1205 self.iList = self.Listing() 1206 1207 info = [ 1208 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1209 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1210 ] 1211 1212 # add instruments count by type: 1213 for iType in self.iList.keys(): 1214 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1215 1216 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1217 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1218 1219 # generating info tables with all instruments by type: 1220 for iType in self.iList.keys(): 1221 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1222 1223 for instrument in self.iList[iType].keys(): 1224 iName = self.iList[iType][instrument]["name"] # instrument's name 1225 if len(iName) > 57: 1226 iName = "{}...".format(iName[:54]) # right trim for a long string 1227 1228 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1229 self.iList[iType][instrument]["ticker"], 1230 iName, 1231 self.iList[iType][instrument]["figi"], 1232 self.iList[iType][instrument]["currency"], 1233 self.iList[iType][instrument]["lot"], 1234 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1235 )) 1236 1237 infoText = "".join(info) 1238 1239 if show: 1240 uLogger.info(infoText) 1241 1242 if self.instrumentsFile: 1243 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1244 fH.write(infoText) 1245 1246 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1247 1248 if self.useHTMLReports: 1249 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1250 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1251 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1252 1253 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1254 1255 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse— print only to file.
Returns
multi-lines string with all available broker instruments
1257 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1258 """ 1259 This method search and show information about instruments by part of its ticker, FIGI or name. 1260 If `searchResultsFile` string is not empty then also save information to this file. 1261 1262 :param pattern: string with part of ticker, FIGI or instrument's name. 1263 :param show: if `True` then print results to console, if `False` — return list of result only. 1264 :return: list of dictionaries with all found instruments. 1265 """ 1266 if not self.iList: 1267 self.iList = self.Listing() 1268 1269 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1270 compiledPattern = re.compile(pattern, re.IGNORECASE) 1271 1272 for iType in self.iList: 1273 for instrument in self.iList[iType].values(): 1274 searchResult = compiledPattern.search(" ".join( 1275 [instrument["ticker"], instrument["figi"], instrument["name"]] 1276 )) 1277 1278 if searchResult: 1279 searchResults[iType][instrument["ticker"]] = instrument 1280 1281 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1282 info = [ 1283 "# Search results\n\n", 1284 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1285 "* **Search pattern:** [{}]\n".format(pattern), 1286 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1287 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1288 ] 1289 infoShort = info[:] 1290 1291 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1292 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1293 skippedLine = "| ... | ... | ... | ... |\n" 1294 1295 if resultsLen == 0: 1296 info.append("\nNo results\n") 1297 infoShort.append("\nNo results\n") 1298 uLogger.warning("No results. Try changing your search pattern.") 1299 1300 else: 1301 for iType in searchResults: 1302 iTypeValuesCount = len(searchResults[iType].values()) 1303 if iTypeValuesCount > 0: 1304 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1305 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1306 1307 for instrument in searchResults[iType].values(): 1308 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1309 instrument["type"], 1310 instrument["ticker"], 1311 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1312 instrument["figi"], 1313 )) 1314 1315 if iTypeValuesCount <= 5: 1316 infoShort.extend(info[-iTypeValuesCount:]) 1317 1318 else: 1319 infoShort.extend(info[-5:]) 1320 infoShort.append(skippedLine) 1321 1322 infoText = "".join(info) 1323 infoTextShort = "".join(infoShort) 1324 1325 if show: 1326 uLogger.info(infoTextShort) 1327 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1328 1329 if self.searchResultsFile: 1330 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1331 fH.write(infoText) 1332 1333 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1334 1335 if self.useHTMLReports: 1336 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1337 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1338 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1339 1340 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1341 1342 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse— return list of result only.
Returns
list of dictionaries with all found instruments.
1344 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1345 """ 1346 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1347 1348 :param instruments: list of strings with tickers or FIGIs. 1349 :return: list with unique instrument FIGIs only. 1350 """ 1351 requestedInstruments = [] 1352 for iName in instruments: 1353 if iName not in self.aliases.keys(): 1354 if iName not in requestedInstruments: 1355 requestedInstruments.append(iName) 1356 1357 else: 1358 if iName not in requestedInstruments: 1359 if self.aliases[iName] not in requestedInstruments: 1360 requestedInstruments.append(self.aliases[iName]) 1361 1362 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1363 1364 onlyUniqueFIGIs = [] 1365 for iName in requestedInstruments: 1366 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1367 continue 1368 1369 self._ticker = iName 1370 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1371 1372 if not iData: 1373 self._ticker = "" 1374 self._figi = iName 1375 1376 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1377 1378 if not iData: 1379 self._figi = "" 1380 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1381 1382 if iData and iData["figi"] not in onlyUniqueFIGIs: 1383 onlyUniqueFIGIs.append(iData["figi"]) 1384 1385 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1386 1387 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1389 def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]: 1390 """ 1391 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1392 1393 See limits: https://tinkoff.github.io/investAPI/limits/ 1394 1395 If `pricesFile` string is not empty then also save information to this file. 1396 1397 :param instruments: list of strings with tickers or FIGIs. 1398 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1399 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1400 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1401 """ 1402 if instruments is None or not instruments: 1403 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1404 raise Exception("Ticker or FIGI required") 1405 1406 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1407 1408 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1409 1410 iList = [] # trying to get info and current prices about all unique instruments: 1411 for self._figi in onlyUniqueFIGIs: 1412 iData = self.SearchByFIGI(requestPrice=True) 1413 iList.append(iData) 1414 1415 self.ShowListOfPrices(iList, show) 1416 1417 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1419 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1420 """ 1421 Show table contains current prices of given instruments. 1422 1423 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1424 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1425 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1426 :return: multilines text in Markdown format as a table contains current prices. 1427 """ 1428 infoText = "" 1429 1430 if show or self.pricesFile: 1431 info = [ 1432 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1433 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1434 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1435 ] 1436 1437 for item in iList: 1438 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1439 item["ticker"], 1440 item["figi"], 1441 item["type"], 1442 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1443 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1444 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1445 "{} / {}".format( 1446 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1447 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1448 ), 1449 "{} / {}".format( 1450 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1451 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1452 ), 1453 item["currency"], 1454 )) 1455 1456 infoText = "".join(info) 1457 1458 if show: 1459 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1460 1461 if self.pricesFile: 1462 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1463 fH.write(infoText) 1464 1465 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1466 1467 if self.useHTMLReports: 1468 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1469 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1470 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1471 1472 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1473 1474 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1476 def RequestTradingStatus(self) -> dict: 1477 """ 1478 Requesting trading status for the instrument defined by `figi` variable. 1479 1480 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1481 1482 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1483 1484 :return: dictionary with trading status attributes. Response example: 1485 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1486 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1487 """ 1488 if self._figi is None or not self._figi: 1489 uLogger.error("Variable `figi` must be defined for using this method!") 1490 raise Exception("FIGI required") 1491 1492 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1493 1494 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1495 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1496 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1497 1498 if self.moreDebug: 1499 uLogger.debug("Records about current trading status successfully received") 1500 1501 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1503 def RequestPortfolio(self) -> dict: 1504 """ 1505 Requesting actual user's portfolio for current `accountId`. 1506 1507 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1508 1509 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1510 1511 :return: dictionary with user's portfolio. 1512 """ 1513 if self.accountId is None or not self.accountId: 1514 uLogger.error("Variable `accountId` must be defined for using this method!") 1515 raise Exception("Account ID required") 1516 1517 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1518 1519 self.body = str({"accountId": self.accountId}) 1520 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1521 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1522 1523 if self.moreDebug: 1524 uLogger.debug("Records about user's portfolio successfully received") 1525 1526 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1528 def RequestPositions(self) -> dict: 1529 """ 1530 Requesting open positions by currencies and instruments for current `accountId`. 1531 1532 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1533 1534 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1535 1536 :return: dictionary with open positions by instruments. 1537 """ 1538 if self.accountId is None or not self.accountId: 1539 uLogger.error("Variable `accountId` must be defined for using this method!") 1540 raise Exception("Account ID required") 1541 1542 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1543 1544 self.body = str({"accountId": self.accountId}) 1545 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1546 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1547 1548 if self.moreDebug: 1549 uLogger.debug("Records about current open positions successfully received") 1550 1551 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1553 def RequestPendingOrders(self) -> list: 1554 """ 1555 Requesting current actual pending limit orders for current `accountId`. 1556 1557 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1558 1559 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1560 1561 :return: list of dictionaries with pending limit orders. 1562 """ 1563 if self.accountId is None or not self.accountId: 1564 uLogger.error("Variable `accountId` must be defined for using this method!") 1565 raise Exception("Account ID required") 1566 1567 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1568 1569 self.body = str({"accountId": self.accountId}) 1570 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1571 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1572 1573 if "orders" in rawResponse.keys(): 1574 rawOrders = rawResponse["orders"] 1575 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1576 1577 else: 1578 rawOrders = [] 1579 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1580 1581 return rawOrders
Requesting current actual pending limit orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending limit orders.
1583 def RequestStopOrders(self) -> list: 1584 """ 1585 Requesting current actual stop orders for current `accountId`. 1586 1587 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1588 1589 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1590 1591 :return: list of dictionaries with stop orders. 1592 """ 1593 if self.accountId is None or not self.accountId: 1594 uLogger.error("Variable `accountId` must be defined for using this method!") 1595 raise Exception("Account ID required") 1596 1597 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1598 1599 self.body = str({"accountId": self.accountId}) 1600 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1601 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1602 1603 if "stopOrders" in rawResponse.keys(): 1604 rawStopOrders = rawResponse["stopOrders"] 1605 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1606 1607 else: 1608 rawStopOrders = [] 1609 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1610 1611 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1613 def Overview(self, show: bool = False, details: str = "full") -> dict: 1614 """ 1615 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1616 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1617 and `overviewBondsCalendarFile` are defined then also save information to file. 1618 1619 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1620 many requests about the state of the portfolio, and then, based on the received data, a large number 1621 of calculation and statistics are collected. 1622 1623 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1624 :param details: how detailed should the information be? 1625 - `full` — shows full available information about portfolio status (by default), 1626 - `positions` — shows only open positions, 1627 - `orders` — shows only sections of open limits and stop orders. 1628 - `digest` — show a short digest of the portfolio status, 1629 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1630 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1631 :return: dictionary with client's raw portfolio and some statistics. 1632 """ 1633 if self.accountId is None or not self.accountId: 1634 uLogger.error("Variable `accountId` must be defined for using this method!") 1635 raise Exception("Account ID required") 1636 1637 view = { 1638 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1639 "headers": {}, # list of dictionaries, response headers without "positions" section 1640 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1641 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1642 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1643 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1644 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1645 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1646 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1647 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1648 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1649 }, 1650 "stat": { # --- some statistics calculated using "raw" sections: 1651 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1652 "availableRUB": 0., # available rubles (without other currencies) 1653 "blockedRUB": 0., # blocked sum in Russian Rouble 1654 "totalChangesRUB": 0., # changes for all open trades in RUB 1655 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1656 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1657 "sharesCostRUB": 0., # costs of all shares in RUB 1658 "bondsCostRUB": 0., # costs of all bonds in RUB 1659 "etfsCostRUB": 0., # costs of all etfs in RUB 1660 "futuresCostRUB": 0., # costs of all futures in RUB 1661 "Currencies": [], # list of dictionaries of all currencies statistics 1662 "Shares": [], # list of dictionaries of all shares statistics 1663 "Bonds": [], # list of dictionaries of all bonds statistics 1664 "Etfs": [], # list of dictionaries of all etfs statistics 1665 "Futures": [], # list of dictionaries of all futures statistics 1666 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1667 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1668 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1669 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1670 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1671 }, 1672 "analytics": { # --- some analytics of portfolio: 1673 "distrByAssets": {}, # portfolio distribution by assets 1674 "distrByCompanies": {}, # portfolio distribution by companies 1675 "distrBySectors": {}, # portfolio distribution by sectors 1676 "distrByCurrencies": {}, # portfolio distribution by currencies 1677 "distrByCountries": {}, # portfolio distribution by countries 1678 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1679 } 1680 } 1681 1682 details = details.lower() 1683 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1684 if details not in availableDetails: 1685 details = "full" 1686 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1687 1688 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1689 1690 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1691 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1692 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1693 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1694 1695 # save response headers without "positions" section: 1696 for key in portfolioResponse.keys(): 1697 if key != "positions": 1698 view["raw"]["headers"][key] = portfolioResponse[key] 1699 1700 else: 1701 continue 1702 1703 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1704 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1705 for item in portfolioResponse["positions"]: 1706 if item["instrumentType"] == "currency": 1707 self._figi = item["figi"] 1708 if not self._figi and item["ticker"]: 1709 self._ticker = item["ticker"] 1710 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1711 1712 curr = self.SearchByFIGI(requestPrice=False) 1713 1714 # current price of currency in RUB: 1715 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1716 "name": curr["name"], 1717 "currentPrice": NanoToFloat( 1718 item["currentPrice"]["units"], 1719 item["currentPrice"]["nano"] 1720 ), 1721 } 1722 1723 view["raw"]["Currencies"].append(item) 1724 1725 elif item["instrumentType"] == "share": 1726 view["raw"]["Shares"].append(item) 1727 1728 elif item["instrumentType"] == "bond": 1729 view["raw"]["Bonds"].append(item) 1730 1731 elif item["instrumentType"] == "etf": 1732 view["raw"]["Etfs"].append(item) 1733 1734 elif item["instrumentType"] == "futures": 1735 view["raw"]["Futures"].append(item) 1736 1737 else: 1738 continue 1739 1740 # how many volume of currencies (by ISO currency name) are blocked: 1741 for item in view["raw"]["positions"]["blocked"]: 1742 blocked = NanoToFloat(item["units"], item["nano"]) 1743 if blocked > 0: 1744 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1745 1746 # how many volume of instruments (by FIGI) are blocked: 1747 for item in view["raw"]["positions"]["securities"]: 1748 blocked = int(item["blocked"]) 1749 if blocked > 0: 1750 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1751 1752 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1753 1754 if "rub" in allBlocked.keys(): 1755 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1756 1757 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1758 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1759 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1760 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1761 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1762 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1763 view["stat"]["portfolioCostRUB"] = sum([ 1764 view["stat"]["allCurrenciesCostRUB"], 1765 view["stat"]["sharesCostRUB"], 1766 view["stat"]["bondsCostRUB"], 1767 view["stat"]["etfsCostRUB"], 1768 view["stat"]["futuresCostRUB"], 1769 ]) 1770 1771 # --- calculating some portfolio statistics: 1772 byComp = {} # distribution by companies 1773 bySect = {} # distribution by sectors 1774 byCurr = {} # distribution by currencies (include RUB) 1775 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1776 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1777 1778 for item in portfolioResponse["positions"]: 1779 self._figi = item["figi"] 1780 if not self._figi and item["ticker"]: 1781 self._ticker = item["ticker"] 1782 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1783 1784 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1785 1786 if instrument: 1787 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1788 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1789 1790 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1791 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1792 1793 else: 1794 blocked = 0 1795 1796 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1797 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1798 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1799 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1800 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1801 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1802 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1803 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1804 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1805 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1806 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1807 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1808 1809 statData = { 1810 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1811 "ticker": instrument["ticker"], # ticker by FIGI 1812 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1813 "volume": volume, # available volume of instrument 1814 "lots": lots, # volume in lots of instrument 1815 "direction": direction, # direction of an instrument's position: short or long 1816 "blocked": blocked, # blocked volume of currency or instrument 1817 "currentPrice": curPrice, # current instrument's price in basic asset 1818 "average": average, # current average position price 1819 "cost": cost, # current cost of all volume of instrument in basic asset 1820 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1821 "costRUB": costRUB, # cost of instrument in ruble 1822 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1823 "profit": profit, # expected profit at current moment 1824 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1825 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1826 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1827 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1828 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1829 "step": instrument["step"], # minimum price increment 1830 } 1831 1832 # adding distribution by unique countries: 1833 if statData["country"] not in byCountry.keys(): 1834 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1835 1836 else: 1837 byCountry[statData["country"]]["cost"] += costRUB 1838 byCountry[statData["country"]]["percent"] += percentCostRUB 1839 1840 if item["instrumentType"] != "currency": 1841 # adding distribution by unique companies: 1842 if statData["name"]: 1843 if statData["name"] not in byComp.keys(): 1844 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1845 1846 else: 1847 byComp[statData["name"]]["cost"] += costRUB 1848 byComp[statData["name"]]["percent"] += percentCostRUB 1849 1850 # adding distribution by unique sectors: 1851 if statData["sector"] not in bySect.keys(): 1852 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1853 1854 else: 1855 bySect[statData["sector"]]["cost"] += costRUB 1856 bySect[statData["sector"]]["percent"] += percentCostRUB 1857 1858 # adding distribution by unique currencies: 1859 if currency not in byCurr.keys(): 1860 byCurr[currency] = { 1861 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1862 "cost": costRUB, 1863 "percent": percentCostRUB 1864 } 1865 1866 else: 1867 byCurr[currency]["cost"] += costRUB 1868 byCurr[currency]["percent"] += percentCostRUB 1869 1870 # saving statistics for every instrument: 1871 if item["instrumentType"] == "currency": 1872 view["stat"]["Currencies"].append(statData) 1873 1874 # update dict with free funds for trading (total - blocked) by currencies 1875 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1876 view["stat"]["funds"][currency] = { 1877 "total": volume, 1878 "totalCostRUB": costRUB, # total volume cost in rubles 1879 "free": volume - blocked, 1880 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1881 } 1882 1883 elif item["instrumentType"] == "share": 1884 view["stat"]["Shares"].append(statData) 1885 1886 elif item["instrumentType"] == "bond": 1887 view["stat"]["Bonds"].append(statData) 1888 1889 elif item["instrumentType"] == "etf": 1890 view["stat"]["Etfs"].append(statData) 1891 1892 elif item["instrumentType"] == "Futures": 1893 view["stat"]["Futures"].append(statData) 1894 1895 else: 1896 continue 1897 1898 # total changes in Russian Ruble: 1899 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1900 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1901 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1902 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1903 view["stat"]["funds"]["rub"] = { 1904 "total": view["stat"]["availableRUB"], 1905 "totalCostRUB": view["stat"]["availableRUB"], 1906 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1907 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1908 } 1909 1910 # --- pending limit orders sector data: 1911 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1912 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1913 1914 for item in view["raw"]["orders"]: 1915 self._figi = item["figi"] 1916 1917 if item["figi"] not in uniquePendingOrdersFIGIs: 1918 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1919 1920 uniquePendingOrdersFIGIs.append(item["figi"]) 1921 uniquePendingOrders[item["figi"]] = instrument 1922 1923 else: 1924 instrument = uniquePendingOrders[item["figi"]] 1925 1926 if instrument: 1927 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1928 orderType = TKS_ORDER_TYPES[item["orderType"]] 1929 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1930 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1931 1932 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1933 if item["direction"] == "ORDER_DIRECTION_BUY": 1934 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1935 1936 else: 1937 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1938 1939 # requested price for order execution: 1940 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1941 1942 # necessary changes in percent to reach target from current price: 1943 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1944 1945 view["stat"]["orders"].append({ 1946 "orderID": item["orderId"], # orderId number parameter of current order 1947 "figi": item["figi"], # FIGI identification 1948 "ticker": instrument["ticker"], # ticker name by FIGI 1949 "lotsRequested": item["lotsRequested"], # requested lots value 1950 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1951 "currentPrice": lastPrice, # current instrument's price for defined action 1952 "targetPrice": target, # requested price for order execution in base currency 1953 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1954 "percentChanges": changes, # changes in percent to target from current price 1955 "currency": item["currency"], # instrument's currency name 1956 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1957 "type": orderType, # type of order from TKS_ORDER_TYPES 1958 "status": orderState, # order status from TKS_ORDER_STATES 1959 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1960 }) 1961 1962 # --- stop orders sector data: 1963 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1964 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1965 1966 for item in view["raw"]["stopOrders"]: 1967 self._figi = item["figi"] 1968 1969 if item["figi"] not in uniqueStopOrdersFIGIs: 1970 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1971 1972 uniqueStopOrdersFIGIs.append(item["figi"]) 1973 uniqueStopOrders[item["figi"]] = instrument 1974 1975 else: 1976 instrument = uniqueStopOrders[item["figi"]] 1977 1978 if instrument: 1979 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1980 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1981 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1982 1983 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1984 if "expirationTime" in item.keys(): 1985 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1986 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1987 1988 else: 1989 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1990 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1991 1992 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1993 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1994 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1995 1996 else: 1997 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1998 1999 # requested price when stop-order executed: 2000 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2001 2002 # price for limit-order, set up when stop-order executed: 2003 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2004 2005 # necessary changes in percent to reach target from current price: 2006 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2007 2008 view["stat"]["stopOrders"].append({ 2009 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2010 "figi": item["figi"], # FIGI identification 2011 "ticker": instrument["ticker"], # ticker name by FIGI 2012 "lotsRequested": item["lotsRequested"], # requested lots value 2013 "currentPrice": lastPrice, # current instrument's price for defined action 2014 "targetPrice": target, # requested price for stop-order execution in base currency 2015 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2016 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2017 "percentChanges": changes, # changes in percent to target from current price 2018 "currency": item["currency"], # instrument's currency name 2019 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2020 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2021 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2022 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2023 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2024 }) 2025 2026 # --- calculating data for analytics section: 2027 # portfolio distribution by assets: 2028 view["analytics"]["distrByAssets"] = { 2029 "Ruble": { 2030 "uniques": 1, 2031 "cost": view["stat"]["availableRUB"], 2032 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2033 }, 2034 "Currencies": { 2035 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2036 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2037 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2038 }, 2039 "Shares": { 2040 "uniques": len(view["stat"]["Shares"]), 2041 "cost": view["stat"]["sharesCostRUB"], 2042 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2043 }, 2044 "Bonds": { 2045 "uniques": len(view["stat"]["Bonds"]), 2046 "cost": view["stat"]["bondsCostRUB"], 2047 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2048 }, 2049 "Etfs": { 2050 "uniques": len(view["stat"]["Etfs"]), 2051 "cost": view["stat"]["etfsCostRUB"], 2052 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2053 }, 2054 "Futures": { 2055 "uniques": len(view["stat"]["Futures"]), 2056 "cost": view["stat"]["futuresCostRUB"], 2057 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2058 }, 2059 } 2060 2061 # portfolio distribution by companies: 2062 view["analytics"]["distrByCompanies"]["All money cash"] = { 2063 "ticker": "", 2064 "cost": view["stat"]["allCurrenciesCostRUB"], 2065 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2066 } 2067 view["analytics"]["distrByCompanies"].update(byComp) 2068 2069 # portfolio distribution by sectors: 2070 view["analytics"]["distrBySectors"]["All money cash"] = { 2071 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2072 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2073 } 2074 view["analytics"]["distrBySectors"].update(bySect) 2075 2076 # portfolio distribution by currencies: 2077 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2078 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2079 2080 if self.moreDebug: 2081 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2082 2083 view["analytics"]["distrByCurrencies"].update(byCurr) 2084 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2085 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2086 2087 # portfolio distribution by countries: 2088 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2089 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2090 2091 if self.moreDebug: 2092 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2093 2094 view["analytics"]["distrByCountries"].update(byCountry) 2095 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2096 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2097 2098 # --- Prepare text statistics overview in human-readable: 2099 if show: 2100 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2101 2102 # Whatever the value `details`, header not changes: 2103 info = [ 2104 "# Client's portfolio\n\n", 2105 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2106 "* **Account ID:** [{}]\n".format(self.accountId), 2107 ] 2108 2109 if details in ["full", "positions", "digest"]: 2110 info.extend([ 2111 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2112 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2113 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2114 view["stat"]["totalChangesRUB"], 2115 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2116 view["stat"]["totalChangesPercentRUB"], 2117 ), 2118 ]) 2119 2120 if details in ["full", "positions"]: 2121 info.extend([ 2122 "## Open positions\n\n", 2123 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2124 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2125 "| **Ruble:** | {:>31} | | | | | |\n".format( 2126 "{:.2f} ({:.2f}) rub".format( 2127 view["stat"]["availableRUB"], 2128 view["stat"]["blockedRUB"], 2129 ) 2130 ) 2131 ]) 2132 2133 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2134 return [ 2135 "| | | | | | | |\n", 2136 "| {:<27} | | | | | {:>19} | |\n".format( 2137 noTradeStr if noTradeStr else typeStr, 2138 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2139 ), 2140 ] 2141 2142 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2143 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2144 "{} [{}]".format(data["ticker"], data["figi"]), 2145 "{:.2f} ({:.2f}) {}".format( 2146 data["volume"], 2147 data["blocked"], 2148 data["currency"], 2149 ) if isCurr else "{:.0f} ({:.0f})".format( 2150 data["volume"], 2151 data["blocked"], 2152 ), 2153 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2154 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2155 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2156 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2157 "{}{:.2f} {} ({}{:.2f}%)".format( 2158 "+" if data["profit"] > 0 else "", 2159 data["profit"], data["baseCurrencyName"], 2160 "+" if data["percentProfit"] > 0 else "", 2161 data["percentProfit"], 2162 ), 2163 ) 2164 2165 # --- Show currencies section: 2166 if view["stat"]["Currencies"]: 2167 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2168 for item in view["stat"]["Currencies"]: 2169 info.append(_InfoStr(item, isCurr=True)) 2170 2171 else: 2172 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2173 2174 # --- Show shares section: 2175 if view["stat"]["Shares"]: 2176 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2177 2178 for item in view["stat"]["Shares"]: 2179 info.append(_InfoStr(item)) 2180 2181 else: 2182 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2183 2184 # --- Show bonds section: 2185 if view["stat"]["Bonds"]: 2186 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2187 2188 for item in view["stat"]["Bonds"]: 2189 info.append(_InfoStr(item)) 2190 2191 else: 2192 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2193 2194 # --- Show etfs section: 2195 if view["stat"]["Etfs"]: 2196 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2197 2198 for item in view["stat"]["Etfs"]: 2199 info.append(_InfoStr(item)) 2200 2201 else: 2202 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2203 2204 # --- Show futures section: 2205 if view["stat"]["Futures"]: 2206 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2207 2208 for item in view["stat"]["Futures"]: 2209 info.append(_InfoStr(item)) 2210 2211 else: 2212 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2213 2214 if details in ["full", "orders"]: 2215 # --- Show pending limit orders section: 2216 if view["stat"]["orders"]: 2217 info.extend([ 2218 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2219 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2220 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2221 ]) 2222 2223 for item in view["stat"]["orders"]: 2224 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2225 "{} [{}]".format(item["ticker"], item["figi"]), 2226 item["orderID"], 2227 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2228 "{} {} ({}{:.2f}%)".format( 2229 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2230 item["baseCurrencyName"], 2231 "+" if item["percentChanges"] > 0 else "", 2232 float(item["percentChanges"]), 2233 ), 2234 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2235 item["action"], 2236 item["type"], 2237 item["date"], 2238 )) 2239 2240 else: 2241 info.append("\n## Total pending limit-orders: [0]\n") 2242 2243 # --- Show stop orders section: 2244 if view["stat"]["stopOrders"]: 2245 info.extend([ 2246 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2247 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2248 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2249 ]) 2250 2251 for item in view["stat"]["stopOrders"]: 2252 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2253 "{} [{}]".format(item["ticker"], item["figi"]), 2254 item["orderID"], 2255 item["lotsRequested"], 2256 "{} {} ({}{:.2f}%)".format( 2257 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2258 item["baseCurrencyName"], 2259 "+" if item["percentChanges"] > 0 else "", 2260 float(item["percentChanges"]), 2261 ), 2262 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2263 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2264 item["action"], 2265 item["type"], 2266 item["expType"], 2267 item["createDate"], 2268 item["expDate"], 2269 )) 2270 2271 else: 2272 info.append("\n## Total stop-orders: [0]\n") 2273 2274 if details in ["full", "analytics"]: 2275 # -- Show analytics section: 2276 if view["stat"]["portfolioCostRUB"] > 0: 2277 info.extend([ 2278 "\n# Analytics\n\n" 2279 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2280 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2281 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2282 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2283 view["stat"]["totalChangesRUB"], 2284 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2285 view["stat"]["totalChangesPercentRUB"], 2286 ), 2287 "\n## Portfolio distribution by assets\n" 2288 "\n| Type | Uniques | Percent | Current cost |\n", 2289 "|------------------------------------|---------|---------|--------------------|\n", 2290 ]) 2291 2292 for key in view["analytics"]["distrByAssets"].keys(): 2293 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2294 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2295 key, 2296 view["analytics"]["distrByAssets"][key]["uniques"], 2297 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2298 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2299 )) 2300 2301 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2302 2303 info.extend([ 2304 "\n## Portfolio distribution by companies\n" 2305 "\n| Company | Percent | Current cost |\n", 2306 aSepLine, 2307 ]) 2308 2309 for company in view["analytics"]["distrByCompanies"].keys(): 2310 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2311 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2312 "{}{}".format( 2313 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2314 company, 2315 ), 2316 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2317 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2318 )) 2319 2320 info.extend([ 2321 "\n## Portfolio distribution by sectors\n" 2322 "\n| Sector | Percent | Current cost |\n", 2323 aSepLine, 2324 ]) 2325 2326 for sector in view["analytics"]["distrBySectors"].keys(): 2327 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2328 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2329 sector, 2330 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2331 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2332 )) 2333 2334 info.extend([ 2335 "\n## Portfolio distribution by currencies\n" 2336 "\n| Instruments currencies | Percent | Current cost |\n", 2337 aSepLine, 2338 ]) 2339 2340 for curr in view["analytics"]["distrByCurrencies"].keys(): 2341 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2342 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2343 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2344 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2345 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2346 )) 2347 2348 info.extend([ 2349 "\n## Portfolio distribution by countries\n" 2350 "\n| Assets by country | Percent | Current cost |\n", 2351 aSepLine, 2352 ]) 2353 2354 for country in view["analytics"]["distrByCountries"].keys(): 2355 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2356 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2357 country, 2358 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2359 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2360 )) 2361 2362 if details in ["full", "calendar"]: 2363 # -- Show bonds payment calendar section: 2364 if view["stat"]["Bonds"]: 2365 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2366 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2367 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2368 2369 else: 2370 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2371 2372 infoText = "".join(info) 2373 2374 uLogger.info(infoText) 2375 2376 if details == "full" and self.overviewFile: 2377 filename = self.overviewFile 2378 2379 elif details == "digest" and self.overviewDigestFile: 2380 filename = self.overviewDigestFile 2381 2382 elif details == "positions" and self.overviewPositionsFile: 2383 filename = self.overviewPositionsFile 2384 2385 elif details == "orders" and self.overviewOrdersFile: 2386 filename = self.overviewOrdersFile 2387 2388 elif details == "analytics" and self.overviewAnalyticsFile: 2389 filename = self.overviewAnalyticsFile 2390 2391 elif details == "calendar" and self.overviewBondsCalendarFile: 2392 filename = self.overviewBondsCalendarFile 2393 2394 else: 2395 filename = "" 2396 2397 if filename: 2398 with open(filename, "w", encoding="UTF-8") as fH: 2399 fH.write(infoText) 2400 2401 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2402 2403 if self.useHTMLReports: 2404 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2405 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2406 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2407 2408 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2409 2410 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
and overviewBondsCalendarFile are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be?
full— shows full available information about portfolio status (by default),positions— shows only open positions,orders— shows only sections of open limits and stop orders.digest— show a short digest of the portfolio status,analytics— shows only the analytics section and the distribution of the portfolio by various categories,calendar— shows only the bonds calendar section (if these present in portfolio),
Returns
dictionary with client's raw portfolio and some statistics.
2412 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2413 """ 2414 Returns history operations between two given dates for current `accountId`. 2415 If `reportFile` string is not empty then also save human-readable report. 2416 Shows some statistical data of closed positions. 2417 2418 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2419 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2420 :param show: if `True` then also prints all records to the console. 2421 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2422 :return: original list of dictionaries with history of deals records from API ("operations" key): 2423 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2424 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2425 """ 2426 if self.accountId is None or not self.accountId: 2427 uLogger.error("Variable `accountId` must be defined for using this method!") 2428 raise Exception("Account ID required") 2429 2430 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2431 2432 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2433 2434 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2435 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2436 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2437 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2438 customStat = {} # custom statistics in additional to responseJSON 2439 2440 # --- output report in human-readable format: 2441 if show or self.reportFile: 2442 splitLine1 = "| | | | | |\n" # Summary section 2443 splitLine2 = "| | | | | | | | |\n" # Operations section 2444 nextDay = "" 2445 2446 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2447 2448 if len(ops) > 0: 2449 customStat = { 2450 "opsCount": 0, # total operations count 2451 "buyCount": 0, # buy operations 2452 "sellCount": 0, # sell operations 2453 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2454 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2455 "payIn": {"rub": 0.}, # Deposit brokerage account 2456 "payOut": {"rub": 0.}, # Withdrawals 2457 "divs": {"rub": 0.}, # Dividends income 2458 "coupons": {"rub": 0.}, # Coupon's income 2459 "brokerCom": {"rub": 0.}, # Service commissions 2460 "serviceCom": {"rub": 0.}, # Service commissions 2461 "marginCom": {"rub": 0.}, # Margin commissions 2462 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2463 } 2464 2465 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2466 for item in ops: 2467 if item["state"] == "OPERATION_STATE_EXECUTED": 2468 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2469 2470 # count buy operations: 2471 if "_BUY" in item["operationType"]: 2472 customStat["buyCount"] += 1 2473 2474 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2475 customStat["buyTotal"][item["payment"]["currency"]] += payment 2476 2477 else: 2478 customStat["buyTotal"][item["payment"]["currency"]] = payment 2479 2480 # count sell operations: 2481 elif "_SELL" in item["operationType"]: 2482 customStat["sellCount"] += 1 2483 2484 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2485 customStat["sellTotal"][item["payment"]["currency"]] += payment 2486 2487 else: 2488 customStat["sellTotal"][item["payment"]["currency"]] = payment 2489 2490 # count incoming operations: 2491 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2492 if item["payment"]["currency"] in customStat["payIn"].keys(): 2493 customStat["payIn"][item["payment"]["currency"]] += payment 2494 2495 else: 2496 customStat["payIn"][item["payment"]["currency"]] = payment 2497 2498 # count withdrawals operations: 2499 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2500 if item["payment"]["currency"] in customStat["payOut"].keys(): 2501 customStat["payOut"][item["payment"]["currency"]] += payment 2502 2503 else: 2504 customStat["payOut"][item["payment"]["currency"]] = payment 2505 2506 # count dividends income: 2507 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2508 if item["payment"]["currency"] in customStat["divs"].keys(): 2509 customStat["divs"][item["payment"]["currency"]] += payment 2510 2511 else: 2512 customStat["divs"][item["payment"]["currency"]] = payment 2513 2514 # count coupon's income: 2515 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2516 if item["payment"]["currency"] in customStat["coupons"].keys(): 2517 customStat["coupons"][item["payment"]["currency"]] += payment 2518 2519 else: 2520 customStat["coupons"][item["payment"]["currency"]] = payment 2521 2522 # count broker commissions: 2523 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2524 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2525 customStat["brokerCom"][item["payment"]["currency"]] += payment 2526 2527 else: 2528 customStat["brokerCom"][item["payment"]["currency"]] = payment 2529 2530 # count service commissions: 2531 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2532 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2533 customStat["serviceCom"][item["payment"]["currency"]] += payment 2534 2535 else: 2536 customStat["serviceCom"][item["payment"]["currency"]] = payment 2537 2538 # count margin commissions: 2539 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2540 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2541 customStat["marginCom"][item["payment"]["currency"]] += payment 2542 2543 else: 2544 customStat["marginCom"][item["payment"]["currency"]] = payment 2545 2546 # count withholding taxes: 2547 elif "_TAX" in item["operationType"]: 2548 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2549 customStat["allTaxes"][item["payment"]["currency"]] += payment 2550 2551 else: 2552 customStat["allTaxes"][item["payment"]["currency"]] = payment 2553 2554 else: 2555 continue 2556 2557 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2558 2559 # --- view "Actions" lines: 2560 info.extend([ 2561 "| Report sections | | | | |\n", 2562 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2563 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2564 "| | Buy: {:<22} | {:<28} | | |\n".format( 2565 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2566 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2567 ), 2568 "| | Sell: {:<21} | {:<28} | | |\n".format( 2569 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2570 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2571 ), 2572 ]) 2573 2574 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2575 for key in opsKeys: 2576 if key == "rub": 2577 continue 2578 2579 info.extend([ 2580 "| | | {:<28} | | |\n".format( 2581 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2582 ), 2583 "| | | {:<28} | | |\n".format( 2584 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2585 ), 2586 ]) 2587 2588 info.append(splitLine1) 2589 2590 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2591 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2592 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2593 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2594 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2595 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2596 ) 2597 2598 # --- view "Payments" lines: 2599 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2600 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2601 2602 for key in paymentsKeys: 2603 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2604 2605 info.append(splitLine1) 2606 2607 # --- view "Commissions and taxes" lines: 2608 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2609 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2610 2611 for key in comKeys: 2612 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2613 2614 info.extend([ 2615 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2616 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2617 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2618 ]) 2619 2620 else: 2621 info.append("Broker returned no operations during this period\n") 2622 2623 # --- view "Operations" section: 2624 for item in ops: 2625 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2626 continue 2627 2628 else: 2629 self._figi = item["figi"] 2630 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2631 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2632 2633 # group of deals during one day: 2634 if nextDay and item["date"].split("T")[0] != nextDay: 2635 info.append(splitLine2) 2636 nextDay = "" 2637 2638 else: 2639 nextDay = item["date"].split("T")[0] # saving current day for splitting 2640 2641 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2642 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2643 self._figi if self._figi else "—", 2644 instrument["ticker"] if instrument else "—", 2645 instrument["type"] if instrument else "—", 2646 item["quantity"] if int(item["quantity"]) > 0 else "—", 2647 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2648 TKS_OPERATION_STATES[item["state"]], 2649 TKS_OPERATION_TYPES[item["operationType"]], 2650 )) 2651 2652 infoText = "".join(info) 2653 2654 if show: 2655 if self.moreDebug: 2656 uLogger.debug("Records about history of a client's operations successfully received") 2657 2658 uLogger.info(infoText) 2659 2660 if self.reportFile: 2661 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2662 fH.write(infoText) 2663 2664 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2665 2666 if self.useHTMLReports: 2667 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2668 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2669 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2670 2671 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2672 2673 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2675 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2676 """ 2677 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2678 2679 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2680 Warning! Broker server used ISO UTC time by default. 2681 2682 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2683 Also, `historyFile` used to update history with `onlyMissing` parameter. 2684 2685 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2686 2687 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2688 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2689 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2690 `"hour"`, `"day"`. Default: `"hour"`. 2691 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2692 False by default. Warning! History appends only from last candle to current time 2693 with always update last candle! 2694 :param csvSep: separator if csv-file is used, `,` by default. 2695 :param show: if `True` then also prints Pandas DataFrame to the console. 2696 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2697 `["date", "time", "open", "high", "low", "close", "volume"]`. 2698 """ 2699 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2700 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2701 history = None # empty pandas object for history 2702 2703 if interval not in TKS_CANDLE_INTERVALS.keys(): 2704 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2705 raise Exception("Incorrect value") 2706 2707 if not (self._ticker or self._figi): 2708 uLogger.error("Ticker or FIGI must be defined!") 2709 raise Exception("Ticker or FIGI required") 2710 2711 if self._ticker and not self._figi: 2712 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2713 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2714 2715 if self._figi and not self._ticker: 2716 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2717 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2718 2719 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2720 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2721 if interval.lower() != "day": 2722 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2723 2724 delta = dtEnd - dtStart # current UTC time minus last time in file 2725 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2726 2727 # calculate history length in candles: 2728 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2729 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2730 length += 1 # to avoid fraction time 2731 2732 # calculate data blocks count: 2733 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2734 2735 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2736 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2737 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2738 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2739 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2740 2741 tempOld = None # pandas object for old history, if --only-missing key present 2742 lastTime = None # datetime object of last old candle in file 2743 2744 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2745 uLogger.debug("--only-missing key present, add only last missing candles...") 2746 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2747 2748 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2749 2750 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2751 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2752 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2753 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2754 2755 # get last datetime object from last string in file or minus 1 delta if file is empty: 2756 if len(tempOld) > 0: 2757 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2758 2759 else: 2760 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2761 2762 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2763 2764 responseJSONs = [] # raw history blocks of data 2765 2766 blockEnd = dtEnd 2767 for item in range(blocks): 2768 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2769 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2770 2771 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2772 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2773 )) 2774 2775 if blockStart == blockEnd: 2776 uLogger.debug("Skipped this zero-length block...") 2777 2778 else: 2779 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2780 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2781 self.body = str({ 2782 "figi": self._figi, 2783 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2784 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2785 "interval": TKS_CANDLE_INTERVALS[interval][0] 2786 }) 2787 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2788 2789 if "code" in responseJSON.keys(): 2790 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2791 2792 else: 2793 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2794 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2795 2796 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2797 2798 blockEnd = blockStart 2799 2800 printCount = len(responseJSONs) # candles to show in console 2801 if responseJSONs: 2802 tempHistory = pd.DataFrame( 2803 data={ 2804 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2805 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2806 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2807 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2808 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2809 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2810 "volume": [int(item["volume"]) for item in responseJSONs], 2811 }, 2812 index=range(len(responseJSONs)), 2813 columns=["date", "time", "open", "high", "low", "close", "volume"], 2814 ) 2815 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2816 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2817 2818 # append only newest candles to old history if --only-missing key present: 2819 if onlyMissing and tempOld is not None and lastTime is not None: 2820 index = 0 # find start index in tempHistory data: 2821 2822 for i, item in tempHistory.iterrows(): 2823 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2824 2825 if curTime == lastTime: 2826 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2827 index = i 2828 printCount = index + 1 2829 break 2830 2831 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2832 2833 else: 2834 history = tempHistory # if no `--only-missing` key then load full data from server 2835 2836 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2837 2838 if history is not None and not history.empty: 2839 if show: 2840 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2841 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2842 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2843 )) 2844 2845 else: 2846 uLogger.warning("Received an empty candles history!") 2847 2848 if self.historyFile is not None: 2849 if history is not None and not history.empty: 2850 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2851 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2852 2853 else: 2854 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2855 2856 else: 2857 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2858 2859 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2861 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2862 """ 2863 Load candles history from csv-file and return Pandas DataFrame object. 2864 2865 See also: `History()` and `ShowHistoryChart()` methods. 2866 2867 :param filePath: path to csv-file to open. 2868 """ 2869 loadedHistory = None # init candles data object 2870 2871 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2872 2873 if os.path.exists(filePath): 2874 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2875 2876 tfStr = self.priceModel.FormattedDelta( 2877 self.priceModel.timeframe, 2878 "{days} days {hours}h {minutes}m {seconds}s", 2879 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2880 self.priceModel.timeframe, 2881 "{hours}h {minutes}m {seconds}s", 2882 ) 2883 2884 if loadedHistory is not None and not loadedHistory.empty: 2885 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2886 len(loadedHistory), 2887 tfStr, 2888 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2889 ) 2890 2891 else: 2892 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2893 2894 else: 2895 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2896 2897 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2899 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2900 """ 2901 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2902 2903 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2904 Default: `index.html` (both for interact and non-interact candlesticks chart). 2905 2906 See also: `History()` and `LoadHistory()` methods. 2907 2908 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2909 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2910 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2911 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2912 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2913 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2914 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2915 """ 2916 if isinstance(candles, str): 2917 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2918 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2919 2920 elif isinstance(candles, pd.DataFrame): 2921 self.priceModel.prices = candles # set candles chain from variable 2922 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2923 2924 if "datetime" not in candles.columns: 2925 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2926 2927 else: 2928 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2929 raise Exception("Incorrect value") 2930 2931 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2932 2933 if interact: 2934 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2935 2936 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2937 2938 else: 2939 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2940 2941 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2942 2943 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2945 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2946 """ 2947 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2948 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2949 2950 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2951 2952 :param operation: string "Buy" or "Sell". 2953 :param lots: volume, integer count of lots >= 1. 2954 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2955 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2956 :param expDate: string "Undefined" by default or local date in future, 2957 it is a string with format `%Y-%m-%d %H:%M:%S`. 2958 :return: JSON with response from broker server. 2959 """ 2960 if self.accountId is None or not self.accountId: 2961 uLogger.error("Variable `accountId` must be defined for using this method!") 2962 raise Exception("Account ID required") 2963 2964 if operation is None or not operation or operation not in ("Buy", "Sell"): 2965 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2966 raise Exception("Incorrect value") 2967 2968 if lots is None or lots < 1: 2969 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2970 lots = 1 2971 2972 if tp is None or tp < 0: 2973 tp = 0 2974 2975 if sl is None or sl < 0: 2976 sl = 0 2977 2978 if expDate is None or not expDate: 2979 expDate = "Undefined" 2980 2981 if not (self._ticker or self._figi): 2982 uLogger.error("Ticker or FIGI must be defined!") 2983 raise Exception("Ticker or FIGI required") 2984 2985 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2986 self._ticker = instrument["ticker"] 2987 self._figi = instrument["figi"] 2988 2989 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 2990 2991 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2992 self.body = str({ 2993 "figi": self._figi, 2994 "quantity": str(lots), 2995 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2996 "accountId": str(self.accountId), 2997 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2998 }) 2999 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3000 3001 if "orderId" in response.keys(): 3002 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3003 operation, response["orderId"], 3004 self._ticker, self._figi, lots, 3005 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3006 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3007 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3008 )) 3009 3010 if tp > 0: 3011 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3012 3013 if sl > 0: 3014 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3015 3016 else: 3017 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3018 3019 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3021 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3022 """ 3023 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3024 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3025 3026 See also: `Order()` and `Trade()` docstrings. 3027 3028 :param lots: volume, integer count of lots >= 1. 3029 :param tp: float > 0, take profit price of stop-order. 3030 :param sl: float > 0, stop loss price of stop-order. 3031 :param expDate: it's a local date in future. 3032 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3033 :return: JSON with response from broker server. 3034 """ 3035 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3037 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3038 """ 3039 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3040 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3041 3042 See also: `Order()` and `Trade()` docstrings. 3043 3044 :param lots: volume, integer count of lots >= 1. 3045 :param tp: float > 0, take profit price of stop-order. 3046 :param sl: float > 0, stop loss price of stop-order. 3047 :param expDate: it's a local date in the future. 3048 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3049 :return: JSON with response from broker server. 3050 """ 3051 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3053 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3054 """ 3055 Close position of given instruments. 3056 3057 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3058 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3059 This avoids unnecessary downloading data from the server. 3060 """ 3061 if instruments is None or not instruments: 3062 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3063 raise Exception("Ticker or FIGI required") 3064 3065 if isinstance(instruments, str): 3066 instruments = [instruments] 3067 3068 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3069 if uniqueInstruments: 3070 if portfolio is None or not portfolio: 3071 portfolio = self.Overview(show=False) 3072 3073 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3074 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3075 3076 for self._figi in uniqueInstruments: 3077 if self._figi not in allOpened: 3078 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3079 continue 3080 3081 # search open trade info about instrument by ticker: 3082 instrument = {} 3083 for iType in TKS_INSTRUMENTS: 3084 if instrument: 3085 break 3086 3087 for item in portfolio["stat"][iType]: 3088 if item["figi"] == self._figi: 3089 instrument = item 3090 break 3091 3092 if instrument: 3093 self._ticker = instrument["ticker"] 3094 self._figi = instrument["figi"] 3095 3096 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3097 self._ticker, 3098 self._figi, 3099 int(instrument["volume"]), 3100 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3101 )) 3102 3103 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3104 3105 if tradeLots > 0: 3106 if instrument["blocked"] > 0: 3107 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3108 instrument["blocked"], 3109 self._ticker, 3110 tradeLots, 3111 )) 3112 3113 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3114 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3115 3116 else: 3117 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3119 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3120 """ 3121 Close all positions of given instruments with defined type. 3122 3123 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3124 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3125 This avoids unnecessary downloading data from the server. 3126 """ 3127 if iType not in TKS_INSTRUMENTS: 3128 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3129 3130 else: 3131 if portfolio is None or not portfolio: 3132 portfolio = self.Overview(show=False) 3133 3134 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3135 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3136 3137 if tickers and portfolio: 3138 self.CloseTrades(tickers, portfolio) 3139 3140 else: 3141 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3143 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3144 """ 3145 Universal method to create market or limit orders with all available parameters for current `accountId`. 3146 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3147 3148 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3149 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3150 3151 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3152 then broker immediately open market order as you can do simple --buy or --sell operations! 3153 3154 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3155 When current price will go up or down to target price value then broker opens a limit order. 3156 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3157 3158 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3159 3160 :param operation: string "Buy" or "Sell". 3161 :param orderType: string "Limit" or "Stop". 3162 :param lots: volume, integer count of lots >= 1. 3163 :param targetPrice: target price > 0. This is open trade price for limit order. 3164 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3165 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3166 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3167 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3168 Stop loss order always executed by market price. 3169 :param expDate: string "Undefined" by default or local date in future. 3170 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3171 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3172 A limit order has no expiration date, it lasts until the end of the trading day. 3173 :return: JSON with response from broker server. 3174 """ 3175 if self.accountId is None or not self.accountId: 3176 uLogger.error("Variable `accountId` must be defined for using this method!") 3177 raise Exception("Account ID required") 3178 3179 if operation is None or not operation or operation not in ("Buy", "Sell"): 3180 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3181 raise Exception("Incorrect value") 3182 3183 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3184 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3185 raise Exception("Incorrect value") 3186 3187 if lots is None or lots < 1: 3188 uLogger.error("You must define trade volume > 0: integer count of lots!") 3189 raise Exception("Incorrect value") 3190 3191 if targetPrice is None or targetPrice <= 0: 3192 uLogger.error("Target price for limit-order must be greater than 0!") 3193 raise Exception("Incorrect value") 3194 3195 if limitPrice is None or limitPrice <= 0: 3196 limitPrice = targetPrice 3197 3198 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3199 stopType = "Limit" 3200 3201 if expDate is None or not expDate: 3202 expDate = "Undefined" 3203 3204 if not (self._ticker or self._figi): 3205 uLogger.error("Tocker or FIGI must be defined!") 3206 raise Exception("Ticker or FIGI required") 3207 3208 response = {} 3209 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3210 self._ticker = instrument["ticker"] 3211 self._figi = instrument["figi"] 3212 3213 if orderType == "Limit": 3214 uLogger.debug( 3215 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3216 self._ticker, self._figi, 3217 operation, lots, targetPrice, instrument["currency"], 3218 )) 3219 3220 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3221 self.body = str({ 3222 "figi": self._figi, 3223 "quantity": str(lots), 3224 "price": FloatToNano(targetPrice), 3225 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3226 "accountId": str(self.accountId), 3227 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3228 }) 3229 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3230 3231 if "orderId" in response.keys(): 3232 uLogger.info( 3233 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3234 response["orderId"], self._ticker, self._figi, operation, lots, 3235 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3236 )) 3237 3238 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3239 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3240 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3241 targetPrice, instrument["currency"], 3242 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3243 )) 3244 3245 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3246 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3247 targetPrice, instrument["currency"], 3248 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3249 )) 3250 3251 else: 3252 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3253 3254 if orderType == "Stop": 3255 uLogger.debug( 3256 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3257 self._ticker, self._figi, 3258 operation, lots, 3259 targetPrice, instrument["currency"], 3260 limitPrice, instrument["currency"], 3261 stopType, expDate, 3262 )) 3263 3264 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3265 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3266 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3267 3268 body = { 3269 "figi": self._figi, 3270 "quantity": str(lots), 3271 "price": FloatToNano(limitPrice), 3272 "stopPrice": FloatToNano(targetPrice), 3273 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3274 "accountId": str(self.accountId), 3275 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3276 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3277 } 3278 3279 if expDateUTC: 3280 body["expireDate"] = expDateUTC 3281 3282 self.body = str(body) 3283 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3284 3285 if "stopOrderId" in response.keys(): 3286 uLogger.info( 3287 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3288 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3289 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3290 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3291 TKS_STOP_ORDER_TYPES[stopOrderType], 3292 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3293 )) 3294 3295 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3296 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3297 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3298 targetPrice, instrument["currency"], 3299 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3300 )) 3301 3302 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3303 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3304 targetPrice, instrument["currency"], 3305 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3306 )) 3307 3308 else: 3309 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3310 3311 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3313 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3314 """ 3315 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3316 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3317 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3318 See also: `Order()` docstring. 3319 3320 :param lots: volume, integer count of lots >= 1. 3321 :param targetPrice: target price > 0. This is open trade price for limit order. 3322 :return: JSON with response from broker server. 3323 """ 3324 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3326 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3327 """ 3328 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3329 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3330 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3331 target price value then broker opens a limit order. See also: `Order()` docstring. 3332 3333 :param lots: volume, integer count of lots >= 1. 3334 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3335 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3336 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3337 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3338 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3339 :param expDate: string "Undefined" by default or local date in future. 3340 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3341 This date is converting to UTC format for server. 3342 :return: JSON with response from broker server. 3343 """ 3344 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3346 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3347 """ 3348 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3349 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3350 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3351 See also: `Order()` docstring. 3352 3353 :param lots: volume, integer count of lots >= 1. 3354 :param targetPrice: target price > 0. This is open trade price for limit order. 3355 :return: JSON with response from broker server. 3356 """ 3357 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3359 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3360 """ 3361 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3362 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3363 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3364 target price value then broker opens a limit order. See also: `Order()` docstring. 3365 3366 :param lots: volume, integer count of lots >= 1. 3367 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3368 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3369 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3370 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3371 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3372 :param expDate: string "Undefined" by default or local date in future. 3373 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3374 This date is converting to UTC format for server. 3375 :return: JSON with response from broker server. 3376 """ 3377 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3379 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3380 """ 3381 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3382 3383 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3384 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3385 This avoids unnecessary downloading data from the server. 3386 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3387 """ 3388 if self.accountId is None or not self.accountId: 3389 uLogger.error("Variable `accountId` must be defined for using this method!") 3390 raise Exception("Account ID required") 3391 3392 if orderIDs: 3393 if allOrdersIDs is None: 3394 rawOrders = self.RequestPendingOrders() 3395 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3396 3397 if allStopOrdersIDs is None: 3398 rawStopOrders = self.RequestStopOrders() 3399 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3400 3401 for orderID in orderIDs: 3402 idInPendingOrders = orderID in allOrdersIDs 3403 idInStopOrders = orderID in allStopOrdersIDs 3404 3405 if not (idInPendingOrders or idInStopOrders): 3406 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3407 continue 3408 3409 else: 3410 if idInPendingOrders: 3411 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3412 3413 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3414 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3415 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3416 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3417 3418 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3419 if self.moreDebug: 3420 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3421 3422 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3423 3424 else: 3425 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3426 3427 elif idInStopOrders: 3428 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3429 3430 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3431 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3432 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3433 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3434 3435 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3436 if self.moreDebug: 3437 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3438 3439 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3440 3441 else: 3442 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3443 3444 else: 3445 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3447 def CloseAllOrders(self) -> None: 3448 """ 3449 Gets a list of open pending and stop orders and cancel it all. 3450 """ 3451 rawOrders = self.RequestPendingOrders() 3452 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3453 lenOrders = len(allOrdersIDs) 3454 3455 rawStopOrders = self.RequestStopOrders() 3456 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3457 lenSOrders = len(allStopOrdersIDs) 3458 3459 if lenOrders > 0 or lenSOrders > 0: 3460 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3461 3462 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3463 3464 else: 3465 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3467 def CloseAll(self, *args) -> None: 3468 """ 3469 Close all available (not blocked) opened trades and orders. 3470 3471 Also, you can select one or more keywords case-insensitive: 3472 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3473 3474 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3475 """ 3476 overview = self.Overview(show=False) # get all open trades info 3477 3478 if len(args) == 0: 3479 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3480 self.CloseAllOrders() # close all pending and stop orders 3481 3482 for iType in TKS_INSTRUMENTS: 3483 if iType != "Currencies": 3484 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3485 3486 else: 3487 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3488 lowerArgs = [x.lower() for x in args] 3489 3490 if "orders" in lowerArgs: 3491 self.CloseAllOrders() # close all pending and stop orders 3492 3493 for iType in TKS_INSTRUMENTS: 3494 if iType.lower() in lowerArgs and iType != "Currencies": 3495 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3497 def CloseAllByTicker(self, instrument: str) -> None: 3498 """ 3499 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3500 3501 This method searches opened trade and orders of instrument throw all portfolio and then use 3502 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3503 3504 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3505 3506 :param instrument: string with ticker. 3507 """ 3508 if instrument is None or not instrument: 3509 uLogger.error("Ticker name must be defined for using this method!") 3510 raise Exception("Ticker required") 3511 3512 overview = self.Overview(show=False) # get user portfolio with all open trades info 3513 3514 self._ticker = instrument # try to set instrument as ticker 3515 self._figi = "" 3516 3517 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3518 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3519 3520 if limitAll and self.IsInLimitOrders(portfolio=overview): 3521 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3522 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3523 3524 if stopAll and self.IsInStopOrders(portfolio=overview): 3525 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3526 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3527 3528 if self.IsInPortfolio(portfolio=overview): 3529 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3530 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with ticker.
3532 def CloseAllByFIGI(self, instrument: str) -> None: 3533 """ 3534 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3535 3536 This method searches opened trade and orders of instrument throw all portfolio and then use 3537 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3538 3539 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3540 3541 :param instrument: string with FIGI id. 3542 """ 3543 if instrument is None or not instrument: 3544 uLogger.error("FIGI id must be defined for using this method!") 3545 raise Exception("FIGI required") 3546 3547 overview = self.Overview(show=False) # get user portfolio with all open trades info 3548 3549 self._ticker = "" 3550 self._figi = instrument # try to set instrument as FIGI id 3551 3552 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3553 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3554 3555 if limitAll and self.IsInLimitOrders(portfolio=overview): 3556 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3557 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3558 3559 if stopAll and self.IsInStopOrders(portfolio=overview): 3560 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3561 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3562 3563 if self.IsInPortfolio(portfolio=overview): 3564 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3565 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with FIGI id.
3567 @staticmethod 3568 def ParseOrderParameters(operation, **inputParameters): 3569 """ 3570 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3571 3572 :param operation: string "Buy" or "Sell". 3573 :param inputParameters: this is dict of strings that looks like this 3574 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3575 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3576 "prices" key: one or more prices to open limit-orders 3577 Counts of values in lots and prices lists must be equals! 3578 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3579 """ 3580 # TODO: update order grid work with api v2 3581 pass 3582 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3583 # 3584 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3585 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3586 # raise Exception("Incorrect value") 3587 # 3588 # if "l" in inputParameters.keys(): 3589 # inputParameters["lots"] = inputParameters.pop("l") 3590 # 3591 # if "p" in inputParameters.keys(): 3592 # inputParameters["prices"] = inputParameters.pop("p") 3593 # 3594 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3595 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3596 # raise Exception("Incorrect value") 3597 # 3598 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3599 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3600 # 3601 # if len(lots) != len(prices): 3602 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3603 # raise Exception("Incorrect value") 3604 # 3605 # uLogger.debug("Extracted parameters for orders:") 3606 # uLogger.debug("lots = {}".format(lots)) 3607 # uLogger.debug("prices = {}".format(prices)) 3608 # 3609 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3610 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3611 # uLogger.debug("Order parameters: {}".format(result)) 3612 # 3613 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3615 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3616 """ 3617 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3618 3619 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3620 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3621 """ 3622 result = False 3623 msg = "Instrument not defined!" 3624 3625 if portfolio is None or not portfolio: 3626 portfolio = self.Overview(show=False) 3627 3628 if self._ticker: 3629 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3630 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3631 3632 for iType in TKS_INSTRUMENTS: 3633 for instrument in portfolio["stat"][iType]: 3634 if instrument["ticker"] == self._ticker: 3635 result = True 3636 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3637 break 3638 3639 elif self._figi: 3640 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3641 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3642 3643 for iType in TKS_INSTRUMENTS: 3644 for instrument in portfolio["stat"][iType]: 3645 if instrument["figi"] == self._figi: 3646 result = True 3647 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3648 break 3649 3650 else: 3651 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3652 3653 uLogger.debug(msg) 3654 3655 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3657 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3658 """ 3659 Returns instrument from the user's portfolio if it presents there. 3660 Instrument must be defined by `ticker` (highly priority) or `figi`. 3661 3662 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3663 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3664 """ 3665 result = None 3666 msg = "Instrument not defined!" 3667 3668 if portfolio is None or not portfolio: 3669 portfolio = self.Overview(show=False) 3670 3671 if self._ticker: 3672 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3673 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3674 3675 for iType in TKS_INSTRUMENTS: 3676 for instrument in portfolio["stat"][iType]: 3677 if instrument["ticker"] == self._ticker: 3678 result = instrument 3679 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3680 break 3681 3682 elif self._figi: 3683 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3684 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3685 3686 for iType in TKS_INSTRUMENTS: 3687 for instrument in portfolio["stat"][iType]: 3688 if instrument["figi"] == self._figi: 3689 result = instrument 3690 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3691 break 3692 3693 else: 3694 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3695 3696 uLogger.debug(msg) 3697 3698 return result
Returns instrument from the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3700 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3701 """ 3702 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3703 3704 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3705 3706 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3707 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3708 """ 3709 result = False 3710 msg = "Instrument not defined!" 3711 3712 if portfolio is None or not portfolio: 3713 portfolio = self.Overview(show=False) 3714 3715 if self._ticker: 3716 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3717 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3718 3719 for instrument in portfolio["stat"]["orders"]: 3720 if instrument["ticker"] == self._ticker: 3721 result = True 3722 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3723 break 3724 3725 elif self._figi: 3726 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3727 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3728 3729 for instrument in portfolio["stat"]["orders"]: 3730 if instrument["figi"] == self._figi: 3731 result = True 3732 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3733 break 3734 3735 else: 3736 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3737 3738 uLogger.debug(msg) 3739 3740 return result
Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif limit orders list contains some limit orders for the instrument,Falseotherwise.
3742 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3743 """ 3744 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3745 Instrument must be defined by `ticker` (highly priority) or `figi`. 3746 3747 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3748 3749 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3750 :return: list with `orderID`s of limit orders. 3751 """ 3752 result = [] 3753 msg = "Instrument not defined!" 3754 3755 if portfolio is None or not portfolio: 3756 portfolio = self.Overview(show=False) 3757 3758 if self._ticker: 3759 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3760 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3761 3762 for instrument in portfolio["stat"]["orders"]: 3763 if instrument["ticker"] == self._ticker: 3764 result.append(instrument["orderID"]) 3765 3766 if result: 3767 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3768 3769 elif self._figi: 3770 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3771 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3772 3773 for instrument in portfolio["stat"]["orders"]: 3774 if instrument["figi"] == self._figi: 3775 result.append(instrument["orderID"]) 3776 3777 if result: 3778 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3779 3780 else: 3781 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3782 3783 uLogger.debug(msg) 3784 3785 return result
Returns list with all orderIDs of opened pending limit orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of limit orders.
3787 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3788 """ 3789 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3790 3791 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3792 3793 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3794 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3795 """ 3796 result = False 3797 msg = "Instrument not defined!" 3798 3799 if portfolio is None or not portfolio: 3800 portfolio = self.Overview(show=False) 3801 3802 if self._ticker: 3803 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3804 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3805 3806 for instrument in portfolio["stat"]["stopOrders"]: 3807 if instrument["ticker"] == self._ticker: 3808 result = True 3809 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3810 break 3811 3812 elif self._figi: 3813 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3814 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3815 3816 for instrument in portfolio["stat"]["stopOrders"]: 3817 if instrument["figi"] == self._figi: 3818 result = True 3819 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3820 break 3821 3822 else: 3823 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3824 3825 uLogger.debug(msg) 3826 3827 return result
Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif stop orders list contains some stop orders for the instrument,Falseotherwise.
3829 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3830 """ 3831 Returns list with all `orderID`s of opened stop orders for the instrument. 3832 Instrument must be defined by `ticker` (highly priority) or `figi`. 3833 3834 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3835 3836 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3837 :return: list with `orderID`s of stop orders. 3838 """ 3839 result = [] 3840 msg = "Instrument not defined!" 3841 3842 if portfolio is None or not portfolio: 3843 portfolio = self.Overview(show=False) 3844 3845 if self._ticker: 3846 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3847 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3848 3849 for instrument in portfolio["stat"]["stopOrders"]: 3850 if instrument["ticker"] == self._ticker: 3851 result.append(instrument["orderID"]) 3852 3853 if result: 3854 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3855 3856 elif self._figi: 3857 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3858 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3859 3860 for instrument in portfolio["stat"]["stopOrders"]: 3861 if instrument["figi"] == self._figi: 3862 result.append(instrument["orderID"]) 3863 3864 if result: 3865 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3866 3867 else: 3868 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3869 3870 uLogger.debug(msg) 3871 3872 return result
Returns list with all orderIDs of opened stop orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of stop orders.
3874 def RequestLimits(self) -> dict: 3875 """ 3876 Method for obtaining the available funds for withdrawal for current `accountId`. 3877 3878 See also: 3879 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3880 - `OverviewLimits()` method 3881 3882 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3883 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3884 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3885 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3886 """ 3887 if self.accountId is None or not self.accountId: 3888 uLogger.error("Variable `accountId` must be defined for using this method!") 3889 raise Exception("Account ID required") 3890 3891 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3892 3893 self.body = str({"accountId": self.accountId}) 3894 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3895 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3896 3897 if self.moreDebug: 3898 uLogger.debug("Records about available funds for withdrawal successfully received") 3899 3900 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3902 def OverviewLimits(self, show: bool = False) -> dict: 3903 """ 3904 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3905 3906 See also: `RequestLimits()`. 3907 3908 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3909 :return: dict with raw parsed data from server and some calculated statistics about it. 3910 """ 3911 if self.accountId is None or not self.accountId: 3912 uLogger.error("Variable `accountId` must be defined for using this method!") 3913 raise Exception("Account ID required") 3914 3915 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3916 3917 view = { 3918 "rawLimits": rawLimits, 3919 "limits": { # parsed data for every currency: 3920 "money": { # this is an array of portfolio currency positions 3921 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3922 }, 3923 "blocked": { # this is an array of blocked currency 3924 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3925 }, 3926 "blockedGuarantee": { # this is locked money under collateral for futures 3927 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3928 }, 3929 }, 3930 } 3931 3932 # --- Prepare text table with limits in human-readable format: 3933 if show: 3934 info = [ 3935 "# Withdrawal limits\n\n", 3936 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3937 "* **Account ID:** [{}]\n".format(self.accountId), 3938 ] 3939 3940 if view["limits"]["money"]: 3941 info.extend([ 3942 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3943 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3944 ]) 3945 3946 else: 3947 info.append("\nNo withdrawal limits\n") 3948 3949 for curr in view["limits"]["money"].keys(): 3950 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3951 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3952 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3953 3954 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3955 "[{}]".format(curr), 3956 "{:.2f}".format(view["limits"]["money"][curr]), 3957 "{:.2f}".format(availableMoney), 3958 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3959 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3960 ) 3961 3962 if curr == "rub": 3963 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3964 3965 else: 3966 info.append(infoStr) 3967 3968 infoText = "".join(info) 3969 3970 uLogger.info(infoText) 3971 3972 if self.withdrawalLimitsFile: 3973 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3974 fH.write(infoText) 3975 3976 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3977 3978 if self.useHTMLReports: 3979 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3980 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3981 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 3982 3983 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 3984 3985 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3987 def RequestAccounts(self) -> dict: 3988 """ 3989 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3990 3991 See also: 3992 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3993 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3994 - `OverviewUserInfo()` method 3995 3996 :return: dict with raw data from server that contains accounts info. Example of dict: 3997 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3998 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3999 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4000 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4001 """ 4002 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4003 4004 self.body = str({}) 4005 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4006 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4007 4008 if self.moreDebug: 4009 uLogger.debug("Records about available accounts successfully received") 4010 4011 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
4013 def RequestUserInfo(self) -> dict: 4014 """ 4015 Method for requesting common user's information. 4016 4017 See also: 4018 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4019 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4020 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4021 - `OverviewUserInfo()` method 4022 4023 :return: dict with raw data from server that contains user's information. Example of dict: 4024 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4025 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4026 """ 4027 uLogger.debug("Requesting common user's information. Wait, please...") 4028 4029 self.body = str({}) 4030 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4031 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4032 4033 if self.moreDebug: 4034 uLogger.debug("Records about current user successfully received") 4035 4036 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
4038 def RequestMarginStatus(self, accountId: str = None) -> dict: 4039 """ 4040 Method for requesting margin calculation for defined account ID. 4041 4042 See also: 4043 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4044 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4045 - `OverviewUserInfo()` method 4046 4047 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4048 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4049 Example of responses: 4050 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4051 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4052 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4053 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4054 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4055 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4056 """ 4057 if accountId is None or not accountId: 4058 if self.accountId is None or not self.accountId: 4059 uLogger.error("Variable `accountId` must be defined for using this method!") 4060 raise Exception("Account ID required") 4061 4062 else: 4063 accountId = self.accountId # use `self.accountId` (main ID) by default 4064 4065 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4066 4067 self.body = str({"accountId": accountId}) 4068 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4069 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4070 4071 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4072 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4073 rawMargin = {} 4074 4075 else: 4076 if self.moreDebug: 4077 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4078 4079 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
4081 def RequestTariffLimits(self) -> dict: 4082 """ 4083 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4084 4085 See also: 4086 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4087 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4088 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4089 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4090 - `OverviewUserInfo()` method 4091 4092 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4093 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4094 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4095 """ 4096 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4097 4098 self.body = str({}) 4099 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4100 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4101 4102 if self.moreDebug: 4103 uLogger.debug("Records with limits of current tariff successfully received") 4104 4105 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
4107 def RequestBondCoupons(self, iJSON: dict) -> dict: 4108 """ 4109 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4110 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4111 All dates are in UTC timezone. 4112 4113 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4114 Documentation: 4115 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4116 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4117 4118 See also: `ExtendBondsData()`. 4119 4120 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4121 If raw iJSON is not data of bond then server returns an error [400] with message: 4122 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4123 :return: dictionary with bond payment calendar. Response example 4124 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4125 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4126 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4127 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4128 """ 4129 if iJSON["figi"] is None or not iJSON["figi"]: 4130 uLogger.error("FIGI must be defined for using this method!") 4131 raise Exception("FIGI required") 4132 4133 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4134 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4135 4136 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4137 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4138 self._figi, 4139 startDate, 4140 endDate, 4141 )) 4142 4143 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4144 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4145 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4146 4147 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4148 uLogger.warning("Instrument type is not bond!") 4149 4150 else: 4151 if self.moreDebug: 4152 uLogger.debug("Records about bond payment calendar successfully received") 4153 4154 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self._ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
4156 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4157 """ 4158 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4159 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4160 coupon yields, current yields and some statistics etc. 4161 4162 WARNING! This is too long operation if a lot of bonds requested from broker server. 4163 4164 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4165 4166 :param instruments: list of strings with tickers or FIGIs. 4167 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4168 for further used by data scientists or stock analytics. 4169 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4170 In XLSX-file and Pandas DataFrame fields mean: 4171 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4172 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4173 """ 4174 if instruments is None or not instruments: 4175 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4176 raise Exception("Ticker or FIGI required") 4177 4178 if isinstance(instruments, str): 4179 instruments = [instruments] 4180 4181 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4182 4183 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4184 4185 iCount = len(uniqueInstruments) 4186 tooLong = iCount >= 20 4187 if tooLong: 4188 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4189 4190 bonds = None 4191 for i, self._figi in enumerate(uniqueInstruments): 4192 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4193 4194 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4195 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4196 rawBond = self.SearchByFIGI(requestPrice=True) 4197 4198 # Widen raw data with UTC current time (iData["actualDateTime"]): 4199 actualDate = datetime.now(tzutc()) 4200 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4201 4202 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4203 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4204 4205 # Replace some values with human-readable: 4206 iData["nominalCurrency"] = iData["nominal"]["currency"] 4207 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4208 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4209 iData["aciCurrency"] = iData["aciValue"]["currency"] 4210 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4211 iData["issueSize"] = int(iData["issueSize"]) 4212 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4213 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4214 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4215 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4216 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4217 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4218 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4219 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4220 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4221 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4222 4223 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4224 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4225 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4226 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4227 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4228 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4229 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4230 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4231 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4232 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4233 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4234 4235 # Widen raw data with calendar data from `rawCalendar` values: 4236 calendarData = [] 4237 if "events" in iData["rawCalendar"].keys(): 4238 for item in iData["rawCalendar"]["events"]: 4239 calendarData.append({ 4240 "couponDate": item["couponDate"], 4241 "couponNumber": int(item["couponNumber"]), 4242 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4243 "payCurrency": item["payOneBond"]["currency"], 4244 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4245 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4246 "couponStartDate": item["couponStartDate"], 4247 "couponEndDate": item["couponEndDate"], 4248 "couponPeriod": item["couponPeriod"], 4249 }) 4250 4251 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4252 if "maturityDate" not in iData.keys(): 4253 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4254 4255 # Widen raw data with Coupon Rate. 4256 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4257 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4258 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4259 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4260 4261 # Widen raw data with Yield to Maturity (YTM) on current date. 4262 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4263 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4264 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4265 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4266 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4267 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4268 4269 iData["calendar"] = calendarData # adds calendar at the end 4270 4271 # Remove not used data: 4272 iData.pop("uid") 4273 iData.pop("positionUid") 4274 iData.pop("currentPrice") 4275 iData.pop("rawCalendar") 4276 4277 colNames = list(iData.keys()) 4278 if bonds is None: 4279 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4280 4281 else: 4282 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4283 4284 else: 4285 uLogger.warning("Instrument is not a bond!") 4286 4287 processed = round(100 * (i + 1) / iCount, 1) 4288 if tooLong and processed % 5 == 0: 4289 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4290 4291 else: 4292 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4293 4294 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4295 4296 # Saving bonds from Pandas DataFrame to XLSX sheet: 4297 if xlsx and self.bondsXLSXFile: 4298 with pd.ExcelWriter( 4299 path=self.bondsXLSXFile, 4300 date_format=TKS_DATE_FORMAT, 4301 datetime_format=TKS_DATE_TIME_FORMAT, 4302 mode="w", 4303 ) as writer: 4304 bonds.to_excel( 4305 writer, 4306 sheet_name="Extended bonds data", 4307 index=True, 4308 encoding="UTF-8", 4309 freeze_panes=(1, 1), 4310 ) # saving as XLSX-file with freeze first row and column as headers 4311 4312 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4313 4314 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4316 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4317 """ 4318 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4319 4320 WARNING! This is too long operation if a lot of bonds requested from broker server. 4321 4322 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4323 4324 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4325 extended information about bonds: main info, current prices, bond payment calendar, 4326 coupon yields, current yields and some statistics etc. 4327 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4328 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4329 for further used by data scientists or stock analytics. 4330 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4331 """ 4332 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4333 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4334 4335 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4336 4337 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4338 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4339 calendar = None 4340 for bond in extBonds.iterrows(): 4341 for item in bond[1]["calendar"]: 4342 cData = { 4343 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4344 "couponDate": item["couponDate"], 4345 "figi": bond[1]["figi"], 4346 "ticker": bond[1]["ticker"], 4347 "name": bond[1]["name"], 4348 "couponNumber": item["couponNumber"], 4349 "payOneBond": item["payOneBond"], 4350 "payCurrency": item["payCurrency"], 4351 "couponType": item["couponType"], 4352 "couponPeriod": item["couponPeriod"], 4353 "fixDate": item["fixDate"], 4354 "couponStartDate": item["couponStartDate"], 4355 "couponEndDate": item["couponEndDate"], 4356 } 4357 4358 if calendar is None: 4359 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4360 4361 else: 4362 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4363 4364 if calendar is not None: 4365 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4366 4367 # Saving calendar from Pandas DataFrame to XLSX sheet: 4368 if xlsx: 4369 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4370 4371 with pd.ExcelWriter( 4372 path=xlsxCalendarFile, 4373 date_format=TKS_DATE_FORMAT, 4374 datetime_format=TKS_DATE_TIME_FORMAT, 4375 mode="w", 4376 ) as writer: 4377 humanReadable = calendar.copy(deep=True) 4378 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4379 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4380 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4381 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4382 humanReadable.columns = colNames # human-readable column names 4383 4384 humanReadable.to_excel( 4385 writer, 4386 sheet_name="Bond payments calendar", 4387 index=False, 4388 encoding="UTF-8", 4389 freeze_panes=(1, 2), 4390 ) # saving as XLSX-file with freeze first row and column as headers 4391 4392 del humanReadable # release df in memory 4393 4394 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4395 4396 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4398 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4399 """ 4400 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4401 Also, creates Markdown file with calendar data, `calendar.md` by default. 4402 4403 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4404 4405 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4406 extended information about bonds: main info, current prices, bond payment calendar, 4407 coupon yields, current yields and some statistics etc. 4408 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4409 :param show: if `True` then also printing bonds payment calendar to the console, 4410 otherwise save to file `calendarFile` only. `False` by default. 4411 :return: multilines text in Markdown format with bonds payment calendar as a table. 4412 """ 4413 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4414 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4415 4416 infoText = "# Bond payments calendar\n\n" 4417 4418 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4419 4420 if not (calendar is None or calendar.empty): 4421 splitLine = "| | | | | | | | | |\n" 4422 4423 info = [ 4424 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4425 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4426 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4427 ] 4428 4429 newMonth = False 4430 notOneBond = calendar["figi"].nunique() > 1 4431 for i, bond in enumerate(calendar.iterrows()): 4432 if newMonth and notOneBond: 4433 info.append(splitLine) 4434 4435 info.append( 4436 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4437 " √" if bond[1]["paid"] else " —", 4438 bond[1]["couponDate"].split("T")[0], 4439 bond[1]["figi"], 4440 bond[1]["ticker"], 4441 bond[1]["couponNumber"], 4442 "{} {}".format( 4443 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4444 bond[1]["payCurrency"], 4445 ), 4446 bond[1]["couponType"], 4447 bond[1]["couponPeriod"], 4448 bond[1]["fixDate"].split("T")[0], 4449 ) 4450 ) 4451 4452 if i < len(calendar.values) - 1: 4453 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4454 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4455 newMonth = False if curDate.month == nextDate.month else True 4456 4457 else: 4458 newMonth = False 4459 4460 infoText += "".join(info) 4461 4462 if show: 4463 uLogger.info("{}".format(infoText)) 4464 4465 if self.calendarFile is not None: 4466 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4467 fH.write(infoText) 4468 4469 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4470 4471 if self.useHTMLReports: 4472 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4473 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4474 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4475 4476 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4477 4478 else: 4479 infoText += "No data\n" 4480 4481 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4483 def OverviewAccounts(self, show: bool = False) -> dict: 4484 """ 4485 Method for parsing and show simple table with all available user accounts. 4486 4487 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4488 4489 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4490 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4491 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4492 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4493 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4494 "closed": "—", "access": "Full access" }, ...}}` 4495 """ 4496 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4497 4498 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4499 accounts = { 4500 item["id"]: { 4501 "type": TKS_ACCOUNT_TYPES[item["type"]], 4502 "name": item["name"], 4503 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4504 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4505 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4506 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4507 } for item in rawAccounts["accounts"] 4508 } 4509 4510 # Raw and parsed data with some fields replaced in "stat" section: 4511 view = { 4512 "rawAccounts": rawAccounts, 4513 "stat": accounts, 4514 } 4515 4516 # --- Prepare simple text table with only accounts data in human-readable format: 4517 if show: 4518 info = [ 4519 "# User accounts\n\n", 4520 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4521 "| Account ID | Type | Status | Name |\n", 4522 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4523 ] 4524 4525 for account in view["stat"].keys(): 4526 info.extend([ 4527 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4528 account, 4529 view["stat"][account]["type"], 4530 view["stat"][account]["status"], 4531 view["stat"][account]["name"], 4532 ) 4533 ]) 4534 4535 infoText = "".join(info) 4536 4537 uLogger.info(infoText) 4538 4539 if self.userAccountsFile: 4540 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4541 fH.write(infoText) 4542 4543 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4544 4545 if self.useHTMLReports: 4546 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4547 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4548 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4549 4550 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4551 4552 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4554 def OverviewUserInfo(self, show: bool = False) -> dict: 4555 """ 4556 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4557 4558 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4559 4560 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4561 :return: dict with raw parsed data from server and some calculated statistics about it. 4562 """ 4563 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4564 tmpTicker = self._ticker 4565 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4566 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4567 self._ticker = tmpTicker 4568 4569 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4570 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4571 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4572 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4573 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4574 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4575 4576 # This is dict with parsed common user data: 4577 userInfo = { 4578 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4579 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4580 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4581 "tariff": rawUserInfo["tariff"], 4582 } 4583 4584 # This is an array of dict with parsed margin statuses for every account IDs: 4585 margins = {} 4586 for accountId in accounts.keys(): 4587 if rawMargins[accountId]: 4588 margins[accountId] = { 4589 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4590 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4591 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4592 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4593 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4594 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4595 "missing": missing["volume"], 4596 } 4597 4598 else: 4599 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4600 4601 unary = {} # unary-connection limits 4602 for item in rawTariffLimits["unaryLimits"]: 4603 if item["limitPerMinute"] in unary.keys(): 4604 unary[item["limitPerMinute"]].extend(item["methods"]) 4605 4606 else: 4607 unary[item["limitPerMinute"]] = item["methods"] 4608 4609 stream = {} # stream-connection limits 4610 for item in rawTariffLimits["streamLimits"]: 4611 if item["limit"] in stream.keys(): 4612 stream[item["limit"]].extend(item["streams"]) 4613 4614 else: 4615 stream[item["limit"]] = item["streams"] 4616 4617 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4618 limits = { 4619 "unary": unary, 4620 "stream": stream, 4621 } 4622 4623 # Raw and parsed data as an output result: 4624 view = { 4625 "rawUserInfo": rawUserInfo, 4626 "rawAccounts": rawAccounts, 4627 "rawMargins": rawMargins, 4628 "rawTariffLimits": rawTariffLimits, 4629 "stat": { 4630 "overview": overview, 4631 "userInfo": userInfo, 4632 "accounts": accounts, 4633 "margins": margins, 4634 "limits": limits, 4635 }, 4636 } 4637 4638 # --- Prepare text table with user information in human-readable format: 4639 if show: 4640 info = [ 4641 "# Full user information\n\n", 4642 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4643 "## Common information\n\n", 4644 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4645 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4646 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4647 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4648 "\n## User accounts\n\n", 4649 ] 4650 4651 for account in view["stat"]["accounts"].keys(): 4652 info.extend([ 4653 "### ID: [{}]\n\n".format(account), 4654 "| Parameters | Values |\n", 4655 "|----------------------|--------------------------------------------------------------|\n", 4656 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4657 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4658 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4659 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4660 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4661 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4662 ]) 4663 4664 if margins[account]: 4665 info.extend([ 4666 "| Margin status: | Enabled |\n", 4667 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4668 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4669 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4670 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4671 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4672 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4673 ]) 4674 4675 else: 4676 info.append("| Margin status: | Disabled |\n\n") 4677 4678 info.extend([ 4679 "\n## Current user tariff limits\n", 4680 "\n### See also\n", 4681 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4682 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4683 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4684 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4685 "\n### Unary limits\n", 4686 ]) 4687 4688 if unary: 4689 for key, values in sorted(unary.items()): 4690 info.append("\n* Max requests per minute: {}\n".format(key)) 4691 4692 for value in values: 4693 info.append(" - {}\n".format(value)) 4694 4695 else: 4696 info.append("\nNot available\n") 4697 4698 info.append("\n### Stream limits\n") 4699 4700 if stream: 4701 for key, values in sorted(stream.items()): 4702 info.append("\n* Max stream connections: {}\n".format(key)) 4703 4704 for value in values: 4705 info.append(" - {}\n".format(value)) 4706 4707 else: 4708 info.append("\nNot available\n") 4709 4710 infoText = "".join(info) 4711 4712 uLogger.info(infoText) 4713 4714 if self.userInfoFile: 4715 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4716 fH.write(infoText) 4717 4718 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4719 4720 if self.useHTMLReports: 4721 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4722 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4723 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4724 4725 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4726 4727 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4730class Args: 4731 """ 4732 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4733 """ 4734 def __init__(self, **kwargs): 4735 self.__dict__.update(kwargs) 4736 4737 def __getattr__(self, item): 4738 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4741def ParseArgs(): 4742 """This function get and parse command line keys.""" 4743 parser = ArgumentParser() # command-line string parser 4744 4745 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4746 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4747 4748 # --- options: 4749 4750 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4751 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4752 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4753 4754 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4755 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4756 4757 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4758 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4759 4760 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4761 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4762 4763 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4764 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4765 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4766 4767 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4768 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4769 4770 # --- commands: 4771 4772 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4773 4774 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4775 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4776 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4777 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4778 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4779 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4780 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4781 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4782 4783 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4784 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4785 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4786 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4787 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4788 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4789 4790 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4791 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4792 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4793 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4794 4795 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4796 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4797 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4798 4799 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4800 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4801 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4802 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4803 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4804 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4805 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4806 4807 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4808 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4809 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4810 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4811 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4812 4813 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4814 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4815 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4816 4817 cmdArgs = parser.parse_args() 4818 return cmdArgs
This function get and parse command line keys.
4821def Main(**kwargs): 4822 """ 4823 Main function for work with TKSBrokerAPI in the console. 4824 4825 See examples: 4826 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4827 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4828 """ 4829 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4830 4831 if args.debug_level: 4832 uLogger.level = 10 # always debug level by default 4833 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4834 4835 exitCode = 0 4836 start = datetime.now(tzutc()) 4837 uLogger.debug("=-" * 50) 4838 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4839 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4840 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4841 )) 4842 4843 # trying to calculate full current version: 4844 buildVersion = __version__ 4845 try: 4846 v = version("tksbrokerapi") 4847 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4848 4849 except Exception: 4850 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4851 4852 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4853 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4854 4855 try: 4856 if args.version: 4857 print("TKSBrokerAPI {}".format(buildVersion)) 4858 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4859 4860 else: 4861 # Init class for trading with Tinkoff Broker: 4862 trader = TinkoffBrokerServer( 4863 token=args.token, 4864 accountId=args.account_id, 4865 useCache=not args.no_cache, 4866 ) 4867 4868 # --- set some options: 4869 4870 if args.more: 4871 trader.moreDebug = True 4872 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4873 4874 if args.html: 4875 trader.useHTMLReports = True 4876 4877 if args.ticker: 4878 ticker = str(args.ticker).upper() # Tickers may be upper case only 4879 4880 if ticker in trader.aliasesKeys: 4881 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4882 4883 else: 4884 trader.ticker = ticker 4885 4886 if args.figi: 4887 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4888 4889 if args.depth is not None: 4890 trader.depth = args.depth 4891 4892 # --- do one command: 4893 4894 if args.list: 4895 if args.output is not None: 4896 trader.instrumentsFile = args.output 4897 4898 trader.ShowInstrumentsInfo(show=True) 4899 4900 elif args.list_xlsx: 4901 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4902 4903 elif args.bonds_xlsx is not None: 4904 if args.output is not None: 4905 trader.bondsXLSXFile = args.output 4906 4907 if len(args.bonds_xlsx) == 0: 4908 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4909 4910 else: 4911 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4912 4913 elif args.search: 4914 if args.output is not None: 4915 trader.searchResultsFile = args.output 4916 4917 trader.SearchInstruments(pattern=args.search[0], show=True) 4918 4919 elif args.info: 4920 if not (args.ticker or args.figi): 4921 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4922 raise Exception("Ticker or FIGI required") 4923 4924 if args.output is not None: 4925 trader.infoFile = args.output 4926 4927 if args.ticker: 4928 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4929 4930 else: 4931 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4932 4933 elif args.calendar is not None: 4934 if args.output is not None: 4935 trader.calendarFile = args.output 4936 4937 if len(args.calendar) == 0: 4938 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4939 4940 else: 4941 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4942 4943 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4944 4945 elif args.price: 4946 if not (args.ticker or args.figi): 4947 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4948 raise Exception("Ticker or FIGI required") 4949 4950 trader.GetCurrentPrices(show=True) 4951 4952 elif args.prices is not None: 4953 if args.output is not None: 4954 trader.pricesFile = args.output 4955 4956 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4957 4958 elif args.overview: 4959 if args.output is not None: 4960 trader.overviewFile = args.output 4961 4962 trader.Overview(show=True, details="full") 4963 4964 elif args.overview_digest: 4965 if args.output is not None: 4966 trader.overviewDigestFile = args.output 4967 4968 trader.Overview(show=True, details="digest") 4969 4970 elif args.overview_positions: 4971 if args.output is not None: 4972 trader.overviewPositionsFile = args.output 4973 4974 trader.Overview(show=True, details="positions") 4975 4976 elif args.overview_orders: 4977 if args.output is not None: 4978 trader.overviewOrdersFile = args.output 4979 4980 trader.Overview(show=True, details="orders") 4981 4982 elif args.overview_analytics: 4983 if args.output is not None: 4984 trader.overviewAnalyticsFile = args.output 4985 4986 trader.Overview(show=True, details="analytics") 4987 4988 elif args.overview_calendar: 4989 if args.output is not None: 4990 trader.overviewAnalyticsFile = args.output 4991 4992 trader.Overview(show=True, details="calendar") 4993 4994 elif args.deals is not None: 4995 if args.output is not None: 4996 trader.reportFile = args.output 4997 4998 if 0 <= len(args.deals) < 3: 4999 trader.Deals( 5000 start=args.deals[0] if len(args.deals) >= 1 else None, 5001 end=args.deals[1] if len(args.deals) == 2 else None, 5002 show=True, # Always show deals report in console 5003 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 5004 ) 5005 5006 else: 5007 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5008 raise Exception("Incorrect value") 5009 5010 elif args.history is not None: 5011 if args.output is not None: 5012 trader.historyFile = args.output 5013 5014 if 0 <= len(args.history) < 3: 5015 dataReceived = trader.History( 5016 start=args.history[0] if len(args.history) >= 1 else None, 5017 end=args.history[1] if len(args.history) == 2 else None, 5018 interval="hour" if args.interval is None or not args.interval else args.interval, 5019 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5020 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5021 show=True, # shows all downloaded candles in console 5022 ) 5023 5024 if args.render_chart is not None and dataReceived is not None: 5025 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5026 5027 trader.ShowHistoryChart( 5028 candles=dataReceived, 5029 interact=iChart, 5030 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5031 ) 5032 5033 else: 5034 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5035 raise Exception("Incorrect value") 5036 5037 elif args.load_history is not None: 5038 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5039 5040 if args.render_chart is not None and histData is not None: 5041 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5042 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5043 5044 trader.ShowHistoryChart( 5045 candles=histData, 5046 interact=iChart, 5047 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5048 ) 5049 5050 elif args.trade is not None: 5051 if 1 <= len(args.trade) <= 5: 5052 trader.Trade( 5053 operation=args.trade[0], 5054 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5055 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5056 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5057 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5058 ) 5059 5060 else: 5061 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5062 5063 elif args.buy is not None: 5064 if 0 <= len(args.buy) <= 4: 5065 trader.Buy( 5066 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5067 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5068 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5069 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5070 ) 5071 5072 else: 5073 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5074 5075 elif args.sell is not None: 5076 if 0 <= len(args.sell) <= 4: 5077 trader.Sell( 5078 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5079 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5080 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5081 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5082 ) 5083 5084 else: 5085 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5086 5087 elif args.order: 5088 if 4 <= len(args.order) <= 7: 5089 trader.Order( 5090 operation=args.order[0], 5091 orderType=args.order[1], 5092 lots=int(args.order[2]), 5093 targetPrice=float(args.order[3]), 5094 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5095 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5096 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5097 ) 5098 5099 else: 5100 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5101 5102 elif args.buy_limit: 5103 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5104 5105 elif args.sell_limit: 5106 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5107 5108 elif args.buy_stop: 5109 if 2 <= len(args.buy_stop) <= 7: 5110 trader.BuyStop( 5111 lots=int(args.buy_stop[0]), 5112 targetPrice=float(args.buy_stop[1]), 5113 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5114 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5115 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5116 ) 5117 5118 else: 5119 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5120 5121 elif args.sell_stop: 5122 if 2 <= len(args.sell_stop) <= 7: 5123 trader.SellStop( 5124 lots=int(args.sell_stop[0]), 5125 targetPrice=float(args.sell_stop[1]), 5126 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5127 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5128 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5129 ) 5130 5131 else: 5132 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5133 5134 # elif args.buy_order_grid is not None: 5135 # # update order grid work with api v2 5136 # if len(args.buy_order_grid) == 2: 5137 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5138 # 5139 # for order in orderParams: 5140 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5141 # 5142 # else: 5143 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5144 # 5145 # elif args.sell_order_grid is not None: 5146 # # update order grid work with api v2 5147 # if len(args.sell_order_grid) >= 2: 5148 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5149 # 5150 # for order in orderParams: 5151 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5152 # 5153 # else: 5154 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5155 5156 elif args.close_order is not None: 5157 trader.CloseOrders(args.close_order) # close only one order 5158 5159 elif args.close_orders is not None: 5160 trader.CloseOrders(args.close_orders) # close list of orders 5161 5162 elif args.close_trade: 5163 if not (args.ticker or args.figi): 5164 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5165 raise Exception("Ticker or FIGI required") 5166 5167 if args.ticker: 5168 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5169 5170 else: 5171 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5172 5173 elif args.close_trades is not None: 5174 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5175 5176 elif args.close_all is not None: 5177 if args.ticker: 5178 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5179 5180 elif args.figi: 5181 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5182 5183 else: 5184 trader.CloseAll(*args.close_all) 5185 5186 elif args.limits: 5187 if args.output is not None: 5188 trader.withdrawalLimitsFile = args.output 5189 5190 trader.OverviewLimits(show=True) 5191 5192 elif args.user_info: 5193 if args.output is not None: 5194 trader.userInfoFile = args.output 5195 5196 trader.OverviewUserInfo(show=True) 5197 5198 elif args.account: 5199 if args.output is not None: 5200 trader.userAccountsFile = args.output 5201 5202 trader.OverviewAccounts(show=True) 5203 5204 else: 5205 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5206 raise Exception("There is no command to execute") 5207 5208 except Exception: 5209 trace = tb.format_exc() 5210 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5211 if e in trace: 5212 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5213 break 5214 5215 uLogger.debug(trace) 5216 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5217 exitCode = 255 # an error occurred, must be open a ticket for this issue 5218 5219 finally: 5220 finish = datetime.now(tzutc()) 5221 5222 if exitCode == 0: 5223 if args.more: 5224 uLogger.debug("All operations were finished success (summary code is 0).") 5225 5226 else: 5227 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5228 os.path.abspath(uLog.defaultLogFile), exitCode, 5229 )) 5230 5231 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5232 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5233 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5234 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5235 )) 5236 uLogger.debug("=-" * 50) 5237 5238 if not kwargs: 5239 sys.exit(exitCode) 5240 5241 else: 5242 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: